chore: sync core lib and CLAUDE.md from agent-core#28
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces security hardening for the binary downloader and extractor, adding SHA-256 checksum verification and strict archive entry validation to prevent path traversal and symlink attacks. Feedback was provided regarding a potential resource leak in the extractZipToScratch function, where a failure during temporary directory creation could result in orphaned directories, and a more robust cleanup pattern was suggested.
| const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-zip-')); | ||
| const tmpZip = path.join(scratch, '__archive.zip'); | ||
| const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-ps-')); | ||
| const scriptPath = path.join(scriptDir, 'extract.ps1'); | ||
|
|
||
| try { | ||
| fs.writeFileSync(tmpZip, buf); | ||
| fs.writeFileSync(scriptPath, EXTRACT_ZIP_PS1, 'utf8'); | ||
|
|
||
| await new Promise(function(resolve, reject) { | ||
| const child = cp.execFile( | ||
| 'powershell.exe', | ||
| [ | ||
| '-NoProfile', | ||
| '-NonInteractive', | ||
| '-ExecutionPolicy', 'Bypass', | ||
| '-File', scriptPath | ||
| ], | ||
| { | ||
| windowsHide: true, | ||
| env: Object.assign({}, process.env, { | ||
| SRC_ZIP: tmpZip, | ||
| DEST_DIR: scratch | ||
| }) | ||
| }, | ||
| function(err, _stdout, stderr) { | ||
| if (err) { | ||
| reject(new Error('zip extraction failed: ' + (stderr || err.message))); | ||
| } else { | ||
| resolve(); | ||
| } | ||
| } | ||
| ); | ||
| // Do not write to stdin; the script reads from env. | ||
| if (child.stdin) child.stdin.end(); | ||
| }); | ||
| ps.on('error', reject); | ||
| }); | ||
|
|
||
| try { fs.unlinkSync(tmpZip); } catch (e) { /* ignore */ } | ||
|
|
||
| // Defense in depth: walkFiles() throws on any symlink/junction. Also | ||
| // confirm every file resolves inside scratch. | ||
| const files = walkFiles(scratch); | ||
| for (let i = 0; i < files.length; i++) { | ||
| assertInsideRoot(scratch, files[i]); | ||
| } | ||
| } catch (err) { | ||
| rmrf(scratch); | ||
| throw err; | ||
| } finally { | ||
| rmrf(scriptDir); | ||
| } | ||
|
|
||
| return scratch; | ||
| } |
There was a problem hiding this comment.
The current implementation of extractZipToScratch has a potential resource leak. If the second fs.mkdtempSync call (for scriptDir) fails, the scratch directory created just before it will not be cleaned up because the code hasn't entered the try block yet. Additionally, the finally block calls rmrf(scriptDir) without checking if scriptDir was successfully initialized, which could lead to a TypeError masking the original error. Moving the resource initialization inside the try block and adding a check in finally ensures robust cleanup.
let scratch;
let scriptDir;
try {
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-zip-'));
const tmpZip = path.join(scratch, '__archive.zip');
scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-ps-'));
const scriptPath = path.join(scriptDir, 'extract.ps1');
fs.writeFileSync(tmpZip, buf);
fs.writeFileSync(scriptPath, EXTRACT_ZIP_PS1, 'utf8');
await new Promise(function(resolve, reject) {
const child = cp.execFile(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-ExecutionPolicy', 'Bypass',
'-File', scriptPath
],
{
windowsHide: true,
env: Object.assign({}, process.env, {
SRC_ZIP: tmpZip,
DEST_DIR: scratch
})
},
function(err, _stdout, stderr) {
if (err) {
reject(new Error('zip extraction failed: ' + (stderr || err.message)));
} else {
resolve();
}
}
);
// Do not write to stdin; the script reads from env.
if (child.stdin) child.stdin.end();
});
try { fs.unlinkSync(tmpZip); } catch (e) { /* ignore */ }
// Defense in depth: walkFiles() throws on any symlink/junction. Also
// confirm every file resolves inside scratch.
const files = walkFiles(scratch);
for (let i = 0; i < files.length; i++) {
assertInsideRoot(scratch, files[i]);
}
const result = scratch;
scratch = null; // Prevent cleanup in catch
return result;
} catch (err) {
if (scratch) rmrf(scratch);
throw err;
} finally {
if (scriptDir) rmrf(scriptDir);
}
}|
Superseded by fresh sync after agent-core upstreamed missing files (workflow-state.js full version + lib/repo-intel/queries.js). Close older PR to avoid conflicts. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 61d39b5. Configure here.
| const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-zip-')); | ||
| const tmpZip = path.join(scratch, '__archive.zip'); | ||
| const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-analyzer-ps-')); | ||
| const scriptPath = path.join(scriptDir, 'extract.ps1'); |
There was a problem hiding this comment.
Scratch directory leaks if scriptDir creation fails
Low Severity
In extractZipToScratch, the scratch directory is created at line 503 and scriptDir at line 505, both before the try block begins at line 508. If the second fs.mkdtempSync call throws (e.g. disk full), execution skips the try-catch-finally entirely, so scratch is never cleaned up. The caller's finally block also can't help because its scratch variable was never assigned. By contrast, extractTarGzToScratch correctly creates only one temp directory right before its try block.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 61d39b5. Configure here.


Automated sync of lib/ and CLAUDE.md from agent-core.
Note
Medium Risk
Touches the runtime download/install path for the
agent-analyzerbinary, so mistakes could break installs across platforms. Risk is mitigated by extensive new tests, but the new checksum/validation and Windows PowerShell extraction flow are behavior-changing.Overview
Adds runtime SHA-256 verification for downloaded
agent-analyzerrelease assets by fetching and enforcing the adjacent.sha256sidecar (with an opt-inskipChecksumescape hatch for local dev only).Hardens installation by extracting archives into an isolated scratch directory, validating archive entry paths to block traversal/absolute/UNC/drive-letter entries, rejecting symlinks, and then copying only the expected binary into
~/.agent-sh/bin.Reworks Windows
.ziphandling to avoidExpand-Archive/command-string parsing by running a dedicated PowerShell script via-Fileand environment variables, with entry-by-entry validation before extraction.Adds a comprehensive
lib/binary/index.test.jssuite covering checksum parsing/verification, path safety checks, scratch cleanup, and basic tar/zip extraction behavior.Reviewed by Cursor Bugbot for commit 61d39b5. Configure here.