fix: neutralize terminal control characters in human-readable output#30
fix: neutralize terminal control characters in human-readable output#30alerizzo wants to merge 2 commits into
Conversation
Repository-derived values (PR/finding titles, author names, branches, file paths, diff/file content, issue messages, package names) were written to the terminal without neutralizing control bytes, so a crafted pull request could inject ANSI/OSC escape sequences to repaint or hide findings, spoof gate status, or trigger terminal side effects (CWE-150). Add sanitizeText() and apply it to every untrusted value *before* the CLI wraps it in its own ansis styling — the console boundary can't be used because the CLI legitimately emits colour codes there. JSON output is left untouched (JSON encoding already escapes control bytes). Fixes OD-309 (HackerOne). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 1 medium (1 false positive) |
| Comprehensibility | 2 minor |
🟢 Metrics 19 complexity · 0 duplication
Metric Results Complexity 19 Duplication 0
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull Request Overview
This PR implements critical protections against terminal control character attacks (CWE-150) by neutralizing potentially dangerous sequences in repository-derived data. While the core sanitization logic is sound, the PR is currently not up to standards due to a visual regression and documentation gaps.
Specifically, the findings command fails to handle optional scanType fields correctly, which will result in the string 'undefined' appearing in the terminal. Additionally, the security instructions in AGENTS.md need to be more granular to ensure consistent enforcement across the codebase. Addressing these will ensure the fix is both secure and maintains high output quality.
Test suggestions
- Found recommended test scenario: Unit tests for sanitizeText verify correct neutralization of ESC, BEL, DEL, CR, and C1 bytes.
- Found recommended test scenario: Unit tests for sanitizeText verify that TAB and LF characters are preserved.
- Found recommended test scenario: Regression test ensures PR metadata (title, author, branch) containing escape sequences is neutralized in terminal output.
- Found recommended test scenario: Regression test ensures annotated diff content containing escape sequences is neutralized in terminal output.
- Found recommended test scenario: Regression test confirms that --output json preserves original escape sequences (escaped via JSON encoding) for downstream tools.
- Found recommended test scenario: Verify sanitization is applied to SCA dependency package names and version strings.
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
| - `ora` for loading spinners | ||
| - `dayjs` for date formatting — for "last updated" style dates, use `formatFriendlyDate()` from `utils/output.ts` (relative for today, "Yesterday", otherwise YYYY-MM-DD) | ||
| - **Output:** Default output is human readable with tables and colors, but can be overridden with the `--output json` flag. | ||
| - **Untrusted output (CWE-150):** Any repository-derived value written to the human-readable output (PR/finding titles, author names, branches, file paths, diff/file content, issue messages, package names, etc.) must be passed through `sanitizeText()` from `utils/sanitize.ts` **before** the CLI wraps it in `ansis` styling. This neutralizes terminal control characters (C0/C1/DEL) so a crafted repo can't inject ANSI/OSC escape sequences. Do **not** sanitize at the `console.log` boundary (that would strip the CLI's own legitimate colors) and do **not** sanitize the `--output json` path (JSON encoding already escapes control bytes). When adding a new command or render helper, sanitize each untrusted field at the point the raw value enters the string. |
There was a problem hiding this comment.
🟡 MEDIUM RISK
The security policy for CWE-150 is currently defined in a complex paragraph using vague references ('etc.'). This should be broken into specific, actionable instructions to ensure consistent application.
| - **Untrusted output (CWE-150):** Any repository-derived value written to the human-readable output (PR/finding titles, author names, branches, file paths, diff/file content, issue messages, package names, etc.) must be passed through `sanitizeText()` from `utils/sanitize.ts` **before** the CLI wraps it in `ansis` styling. This neutralizes terminal control characters (C0/C1/DEL) so a crafted repo can't inject ANSI/OSC escape sequences. Do **not** sanitize at the `console.log` boundary (that would strip the CLI's own legitimate colors) and do **not** sanitize the `--output json` path (JSON encoding already escapes control bytes). When adding a new command or render helper, sanitize each untrusted field at the point the raw value enters the string. | |
| - **Untrusted output (CWE-150: Common Weakness Enumeration for Control Characters):** Any repository-derived value written to the human-readable output must be passed through `sanitizeText()` from `utils/sanitize.ts` **before** the CLI wraps it in `ansis` styling. | |
| - **Sinks to sanitize:** PR/finding titles, author names, branches, file paths, diff/file content, issue messages, and package names. | |
| - **Where NOT to sanitize:** Do not sanitize at the `console.log` boundary (this strips legitimate CLI colors). Do not sanitize for `--output json` (JSON encoding handles escapes). | |
| - **Requirement:** When adding a new command or render helper, sanitize each untrusted field at the point the raw value enters the string. |
There was a problem hiding this comment.
Done in 3324ac2 — split the CWE-150 guidance into granular bullets (sinks / where-not-to-sanitize / when-adding-a-command).
| const catParts = [sanitizeText(item.securityCategory), ansis.dim(sanitizeText(item.scanType))] | ||
| .filter(Boolean) | ||
| .join(" "); |
There was a problem hiding this comment.
🟡 MEDIUM RISK
The item.scanType field is optional. Passing the result of sanitizeText(item.scanType) directly to ansis.dim without checking for existence will cause the string "undefined" or "null" to be printed in the output when the field is missing, as the styling function will stringify its argument.
There was a problem hiding this comment.
Fixed in 3324ac2. Note the stated failure mode isn't reproducible on this ansis version — ansis.dim(undefined) returns "" (falsy), which .filter(Boolean) drops, so no "undefined" was printed. But you're right that the list view didn't guard scanType while the detail view (finding.ts) does, so I've added the item.scanType ? … : undefined guard to make them consistent and future-proof.
| endLine, | ||
| ).catch((e) => { | ||
| console.log("File path: ", issue.filePath); | ||
| console.log("File path: ", sanitizeText(issue.filePath)); |
There was a problem hiding this comment.
⚪ LOW RISK
Suggestion: For better consistency in error reporting, use console.error and apply the same red styling as the adjacent error message.
| console.log("File path: ", sanitizeText(issue.filePath)); | |
| console.error(ansis.red(`File path: ${sanitizeText(issue.filePath)}`)); |
There was a problem hiding this comment.
Done in 3324ac2 — the file-path diagnostic now uses console.error(ansis.red(…)), consistent with the adjacent error line.
|
|
||
| if (item.application) line3Parts.push(ansis.dim(item.application)); | ||
| if (item.application) line3Parts.push(ansis.dim(sanitizeText(item.application))); | ||
| //if (item.affectedTargets) line3Parts.push(ansis.dim(item.affectedTargets)); |
There was a problem hiding this comment.
⚪ LOW RISK
Nitpick: This commented-out line is redundant as affectedTargets is now handled on line 105. Removing it will help keep the code clean.
There was a problem hiding this comment.
Done in 3324ac2 — removed the stale commented-out affectedTargets line.
- findings.ts: guard optional scanType before ansis.dim, matching the finding detail view (defensive; avoids relying on ansis coercing undefined to "") and drop a stale commented-out affectedTargets line. - issue.ts: route the file-path diagnostic through console.error + red, consistent with the adjacent error line. - AGENTS.md: break the CWE-150 output guidance into granular bullets (sinks / where-not-to-sanitize / when-adding-a-command). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
sanitizeText()(src/utils/sanitize.ts) neutralizes C0 (0x00–0x1Fexcept TAB/LF), DEL (0x7F) and C1 (0x80–0x9F) bytes — CR included — replacing each with visible caret /\xNNnotation.ansisstyling, centrally in the shared render helpers (utils/formatting.ts) and at the render points of every command that prints untrusted values (pull-request,findings/finding,issues,issue,repository,ls/directories). Sanitizing at theconsole.logboundary isn't viable — it would strip the CLI's own legitimate colours, and allow-listing SGR would still pass an attacker's SGR through (the originally reported vector).--output jsonis deliberately left untouched — JSON encoding already escapes control bytes, so consumers still get the original values.Test plan
npm test— 490 pass (12 new: 9sanitizeunit tests + 3pull-requestregression tests covering the About table, the annotated diff view, and JSON output staying intact)npx tsc -p tsconfig.build.json --noEmit— cleancodacy pull-request <a PR whose title/diff contains a raw ESC byte>and confirm the escape renders as^[…instead of being interpreted;--output jsonstill shows the raw value.🤖 Generated with Claude Code