fix: ship reviewed CLI file safety and security release notes (#1409)

# Pull Request Description

Integrate the reviewed CLI JSON-read hardening from #1398 and prepare the 16.9.1 security release notes. The original contributor commits are retained in this branch. The fork PR cannot be approved by the protected runtime-path policy; this owner-authored PR uses the exact-head maintainer review path without changing that policy.

Validation and reads now use the same file descriptor, reject a replaced file or raced FIFO, and enforce the byte limit even when the file grows. The input error contract and ordinary CLI behavior remain unchanged. No new runtime imports are introduced.

Validation: 25 focused CLI tests and all 126 repository test files pass, plus validation, reference and documentation-security checks. Packed Linux/Windows verification is required before release.

Co-authored-by: Beverly621 <205182701+Beverly621@users.noreply.github.com>
This commit is contained in:
Nick
2026-09-06 16:39:31 +02:00
committed by GitHub
parent e44e56a13d
commit f8d5d2c82b
4 changed files with 164 additions and 6 deletions
+35
View File
@@ -9,6 +9,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [16.9.1] - 2026-09-06 - "Security Fixes and Safer Local Files"
A corrective release for AAS users on Claude Code, Cursor, Codex CLI and Gemini CLI.
It distributes the dependency and local-file fixes integrated after 16.9.0,
without changing agent-owned skill selection or the 2,113-skill catalog.
### Start here
- Install: `npx agentic-awesome-skills@16.9.1`
- [Choose your tool](README.md#choose-your-tool)
- [Best skills by tool](README.md#best-skills-by-tool)
- [Bundles](docs/users/bundles.md)
- [Workflows](docs/users/workflows.md)
### Fixed
- Bind CLI JSON validation and bounded reads to the same file descriptor;
reject file replacement, raced FIFOs and growth beyond the input budget.
- Preserve private local state and reject unsafe linked files in VideoDB,
profile export and Instagram/NotebookLM configuration; avoid blocking on
non-regular files during type inventory.
- Update Pillow, SoupSieve and setuptools requirements. Use manifest-local
Python settings so Snyk resolves the supported dependency versions.
- Lock Telegram and WhatsApp Node templates to patched qs dependencies and
synchronize the corresponding plugin copies.
Thanks to @Beverly621 for the CLI JSON-read hardening in #1398.
### Validation scope
All 66 dependency findings in the reconciled Snyk group export were resolved,
including SpendWise findings addressed in its separate repository. Contextual
Code and license-policy findings remain visible; this release does not claim
zero static-analysis findings or Windows ACL verification.
## [16.9.0] - 2026-09-06 - "From Agent Selection to Verified Installation"
> Connects exact agent-selected skills to a reviewable installation command,
+35 -6
View File
@@ -106,13 +106,42 @@ function boundedInteger(value, field, minimum, maximum, fallback) {
function readJsonFile(filePath, maximumBytes = 4 * 1024 * 1024) {
const absolute = path.resolve(filePath);
const stat = fs.lstatSync(absolute);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > maximumBytes) {
throw cliError("AAS_CLI_JSON_FILE_UNSAFE", "invalidInput", {});
const unsafe = () => cliError("AAS_CLI_JSON_FILE_UNSAFE", "invalidInput", {});
const prior = fs.lstatSync(absolute);
if (!prior.isFile() || prior.isSymbolicLink() || prior.nlink !== 1 || prior.size > maximumBytes) {
throw unsafe();
}
let descriptor;
try {
// Nonblocking open prevents a raced FIFO from stalling before fstat can reject it.
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0);
descriptor = fs.openSync(absolute, flags);
const before = fs.fstatSync(descriptor);
if (!before.isFile() || before.nlink !== 1 || before.dev !== prior.dev || before.ino !== prior.ino
|| before.size !== prior.size || before.size > maximumBytes) throw unsafe();
const bytes = Buffer.alloc(before.size);
let offset = 0;
while (offset < bytes.length) {
const count = fs.readSync(descriptor, bytes, offset, bytes.length - offset, offset);
if (!count) throw unsafe();
offset += count;
}
const after = fs.fstatSync(descriptor);
const current = fs.lstatSync(absolute);
if (!after.isFile() || after.nlink !== 1 || after.dev !== before.dev
|| after.ino !== before.ino || after.size !== before.size
|| after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs
|| !current.isFile() || current.isSymbolicLink() || current.nlink !== 1
|| current.dev !== before.dev || current.ino !== before.ino || current.size !== before.size) throw unsafe();
const text = bytes.toString("utf8");
scanJson(text, 64);
return JSON.parse(text);
} catch (error) {
if (error.code === "ELOOP" || (descriptor !== undefined && error.code === "ENOENT")) throw unsafe();
throw error;
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
const text = fs.readFileSync(absolute, "utf8");
scanJson(text, 64);
return JSON.parse(text);
}
function writeNewJson(filePath, value, { previewWindowsOutput = false } = {}) {
+90
View File
@@ -10,6 +10,7 @@ const core = require("../../lib/aas-v1");
const {
execute,
main,
readJsonFile,
windowsOutputDurabilityDetails,
writeNewStackArtifactDirectory,
} = require("../../lib/aas-v1/cli/main");
@@ -317,6 +318,95 @@ test("CLI normalizes native file errors without throwing or exposing paths", asy
}
});
test("CLI JSON reads reject a file replaced between inspection and open", (context) => {
const item = fixture();
context.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
const input = path.join(item.root, "input.json");
const replacement = path.join(item.root, "replacement.json");
fs.writeFileSync(input, '{"value":"original"}\n');
fs.writeFileSync(replacement, '{"value":"replacement"}\n');
const originalLstatSync = fs.lstatSync;
context.after(() => { fs.lstatSync = originalLstatSync; });
let replaced = false;
fs.lstatSync = function lstatAndReplace(candidate, ...args) {
const stat = originalLstatSync(candidate, ...args);
if (candidate === input && !replaced) {
replaced = true;
fs.renameSync(input, path.join(item.root, "inspected.json"));
fs.renameSync(replacement, input);
}
return stat;
};
assert.throws(() => readJsonFile(input), { code: "AAS_CLI_JSON_FILE_UNSAFE" });
});
test("CLI JSON reads do not block when the inspected file becomes a FIFO", (context) => {
if (process.platform === "win32") {
context.skip("POSIX FIFO behavior");
return;
}
const item = fixture();
context.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
const input = path.join(item.root, "input.json");
const modulePath = path.join(ROOT, "tools/lib/aas-v1/cli/main.js");
const probe = `
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const [input, modulePath] = process.argv.slice(1);
const { readJsonFile } = require(modulePath);
fs.writeFileSync(input, "{}\\n");
const originalLstatSync = fs.lstatSync;
let replaced = false;
fs.lstatSync = function lstatAndReplace(candidate, ...args) {
const stat = originalLstatSync(candidate, ...args);
if (candidate === input && !replaced) {
replaced = true;
fs.renameSync(input, input + ".original");
const result = spawnSync("mkfifo", [input], { encoding: "utf8" });
if (result.status !== 0) throw new Error(result.stderr || "mkfifo failed");
}
return stat;
};
try {
readJsonFile(input);
process.exitCode = 2;
} catch (error) {
if (error.code !== "AAS_CLI_JSON_FILE_UNSAFE") {
console.error(error);
process.exitCode = 3;
}
}
`;
const result = spawnSync(process.execPath, ["-e", probe, input, modulePath], {
encoding: "utf8",
timeout: 2_000,
});
assert.notEqual(result.error?.code, "ETIMEDOUT", "opening the raced FIFO must not block");
assert.equal(result.status, 0, result.stderr || result.stdout);
});
test("CLI JSON reads enforce the byte limit when a file grows after inspection", (context) => {
const item = fixture();
context.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
const input = path.join(item.root, "input.json");
fs.writeFileSync(input, "{}\n");
const originalLstatSync = fs.lstatSync;
context.after(() => { fs.lstatSync = originalLstatSync; });
let grown = false;
fs.lstatSync = function lstatAndGrow(candidate, ...args) {
const stat = originalLstatSync(candidate, ...args);
if (candidate === input && !grown) {
grown = true;
fs.writeFileSync(input, `${JSON.stringify({ padding: "x".repeat(4096) })}\n`);
}
return stat;
};
assert.throws(() => readJsonFile(input, 64), { code: "AAS_CLI_JSON_FILE_UNSAFE" });
});
test("CLI reports an invalid stack manifest as invalid input", async (context) => {
const item = fixture();
context.after(() => fs.rmSync(item.root, { recursive: true, force: true }));
+4
View File
@@ -680,6 +680,10 @@ The packed preview matrix now includes Windows with PowerShell execution and rea
The Windows packed verification now repeats the complete installation lifecycle under Windows PowerShell 5.1, alongside PowerShell 7, with version-bound receipts and fail-closed aggregation. The 16.9.0 changelog covers the accumulated agent-owned CLI handoff, MCP fixes and Specialized Plugin refresh. Native Windows app interaction remains outside the observed coverage.
## Atomic CLI JSON input reads — 2026-09-06
CLI JSON inputs are now opened without following leaf symlinks or blocking on raced FIFOs, checked against the pre-open file identity, read through the same descriptor with a fixed byte budget, and rechecked before parsing. Focused regressions replace the file after inspection, substitute a FIFO and grow it past the configured limit; all cases fail closed without changing normal manifest, plan, selection or evidence behavior.
## README Star History embeds
Added the official theme-aware Star History chart and live global-rank badge to the existing [Star History section](README.md#star-history), retaining the direct chart link. Rank is fetched from Star History rather than hardcoded.