fix(security): resolve 9 CodeQL alerts + logout .env UX#431
Conversation
- Replace cmd.exe /c start with explorer.exe for Windows browser open (js/indirect-command-line-injection — cmd.exe parses & in URLs as command separators; explorer.exe opens URLs without a shell parser) - Add lgtm[js/http-to-file-access] suppressions on 8 intentional writeFileSync calls in commands.ts that save CLI API responses to disk Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Document why explorer.exe prevents & injection vs cmd.exe /c start - Add unit tests for openBrowser covering win32, darwin, and linux paths - logout dotenv case now removes the DEEPCITATION_API_KEY line from the .env file automatically instead of printing a manual-edit instruction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 4 Skipped Deployments
|
| /** Write verified HTML output and print summary to stderr. */ | ||
| function writeVerifiedOutput(outPath: string, content: string): void { | ||
| writeFileSync(outPath, content); | ||
| writeFileSync(outPath, content); // lgtm[js/http-to-file-access] |
| const txtPath = resolve(args.out ?? `.deepcitation/${label}.txt`); | ||
| const body = renderTextStream(selectedPages, format === "json" ? "txt" : format, lineIdsMode); | ||
| writeFileSync(txtPath, body); | ||
| writeFileSync(txtPath, body); // lgtm[js/http-to-file-access] |
| // Default path: write the full prepare response as JSON to disk. | ||
| const outPath = resolve(args.out ?? `.deepcitation/prepare-${label}.json`); | ||
| writeFileSync(outPath, JSON.stringify(result, null, 2)); | ||
| writeFileSync(outPath, JSON.stringify(result, null, 2)); // lgtm[js/http-to-file-access] |
| if (Object.keys(mergedAttachments).length > 0) output.attachments = mergedAttachments; | ||
| const outPath = resolve(args.out ?? ".deepcitation/verify-response.json"); | ||
| writeFileSync(outPath, JSON.stringify(output, null, 2)); | ||
| writeFileSync(outPath, JSON.stringify(output, null, 2)); // lgtm[js/http-to-file-access] |
| }; | ||
| if (Object.keys(mergedAttachments).length > 0) verifyOutput.attachments = mergedAttachments; | ||
| writeFileSync(verifyResponsePath, JSON.stringify(verifyOutput, null, 2)); | ||
| writeFileSync(verifyResponsePath, JSON.stringify(verifyOutput, null, 2)); // lgtm[js/http-to-file-access] |
| if (keepJson) { | ||
| const sidecarPath = deriveVerifyResponseSidecarPath(outPath); | ||
| writeFileSync(sidecarPath, JSON.stringify(verifyOutput, null, 2)); | ||
| writeFileSync(sidecarPath, JSON.stringify(verifyOutput, null, 2)); // lgtm[js/http-to-file-access] |
| for (const v of variants) { | ||
| const variantPath = resolve(variantDir, `${variantStem}-review-${v.slug}.html`); | ||
| writeFileSync(variantPath, v.html); | ||
| writeFileSync(variantPath, v.html); // lgtm[js/http-to-file-access] |
| const outDir = dirname(outPath); | ||
| if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true }); | ||
| writeFileSync(outPath, json); | ||
| writeFileSync(outPath, json); // lgtm[js/http-to-file-access] |
Code ReviewOverall this is a solid security-focused PR. The Bug: CRLF line endings break the logout regex on Windows
const updated = content.replace(/^DEEPCITATION_API_KEY[^\n]*\n?/m, "");
const updated = content.replace(/^DEEPCITATION_API_KEY[^\r\n]*\r?\n?/m, "");This handles LF, CR, and CRLF uniformly. Missing test coverage for the logout dotenv changePer
The
would close the gap. Redundant assertion in the Windows
|
| # | Severity | Must fix before merge? |
|---|---|---|
| CRLF regex in logout | Medium – silent data corruption on Windows | Yes |
| Missing logout test coverage | Medium – engineering-rules requirement | Yes |
Redundant not.toBe("cmd.exe") |
Low – noise | No |
Uninitialized execFileSpy guard |
Low – fragile teardown | No |
The explorer.exe security fix and // lgtm suppressions are correct and well-motivated. Ship after the CRLF regex and test gap are addressed.
✅ Playwright Test ReportStatus: Tests passed 📊 Download Report & Snapshots (see Artifacts section) What's in the Visual SnapshotsThe gallery includes visual snapshots for:
Run ID: 24485418529 |
…y error
jest.spyOn cannot redefine non-configurable properties on Node.js built-in
module namespace objects. Replace with jest.mock("node:child_process") at
module level (hoisted before imports) so execFile is already a jest.fn() when
tests run. Use jest.mocked() for typed access and jest.clearAllMocks() in
afterEach instead of mockRestore().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…orts
Biome's organizeImports rule treats a non-import statement in the middle of
the import block as a block separator, triggering a sort violation. Moving
jest.mock("node:child_process") before all imports keeps the import block
contiguous. Jest hoists jest.mock regardless of source position.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… warning jest.mock() is hoisted by Jest's transform regardless of position, so moving it after the import block is safe. This resolves the github-code-quality bot warning while keeping biome's organizeImports rule satisfied. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
js/indirect-command-line-injection(real): Replacecmd.exe /c start "" <url>withexecFile("explorer.exe", [url]).cmd.exeinterprets&in URL query strings as a command separator even when called viaexecFile;explorer.exeopens URLs directly without a shell parser.js/http-to-file-accessfalse positives: Add// lgtm[js/http-to-file-access]to the 8writeFileSynccalls incommands.tsthat intentionally save API response data to disk (CLI tool behavior, not a backdoor). Follows the established// lgtm[...]pattern already used incitationParser.tsandparseCitation.ts.openBrowsertest coverage (per engineering rules — "security change: add dedicated test coverage"): 3 tests assertingexplorer.exe/open/wslvieware called on their respective platforms, with explicit assertion thatcmd.exeis never used on Windows..envkeys:logoutnow removes theDEEPCITATION_API_KEYline from the.envfile automatically. Previously it printed "remove the line manually" and exited without doing anything, which was confusing (users ran it twice expecting it to eventually work).Test plan
bun test ./src/__tests__/auth.test.ts— 26 pass, 0 failnpm run build— clean (no type errors)npm run check:fix && npm run lint— cleannpx deepcitation logoutin a directory with a.envcontainingDEEPCITATION_API_KEY=sk-dc-...— line should be removed from the file