fix(security): neutralize CR/LF in rendered log records (CWE-117) - #1270
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
Agent Completion Truth Gate: BLOCKEDReasons: Machine-readable verdict{
"details": {
"collection_errors": [
"incomplete_linked_issue_contract",
"linked_issue_not_agent_task",
"missing_intent_snapshot",
"missing_agent_run_id",
"missing_agent_login"
],
"invalid_fields": [
"policy.agent_login",
"policy.run_id"
]
},
"reasons": [
"invalid_payload"
],
"verdict": "blocked"
} |
There was a problem hiding this comment.
Pull request overview
Hardens rendered logs against CWE-117 injection and carries an unrelated GitHub upload performance optimization.
Changes:
- Escapes line separators in finalized log records.
- Adds rendered-output security regression tests.
- Offloads upload file reading and encoding from the event loop.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
logging_config.py |
Adds centralized log sanitization. |
test_logging_config_crlf.py |
Tests message, traceback, and extra-field sanitization. |
deployment_manager.py |
Moves file reading and encoding to worker threads. |
test_deployment_manager.py |
Tests upload concurrency, integrity, errors, and cancellation. |
Suppressed comments (2)
src/youtube_extension/backend/config/logging_config.py:34
- The separator table still omits U+001C (file separator), U+001D (group separator), and U+001E (record separator). Python treats all three as line boundaries, and U+001E is also used as a record delimiter, so attacker-controlled values containing them can still split downstream records despite the stated acceptance criterion. Escape these characters and include them in the regression input.
}
src/youtube_extension/backend/config/logging_config.py:31
- With
enable_json_logging=True, these replacements are interpolated directly into a JSON string, but\v,\x1b, and\x85are not valid JSON escapes. A record containing any of these characters remains single-line but becomes unparsable, allowing attacker-controlled input to corrupt or drop downstream JSON logs. Encode them as JSON-compatible\u000b,\u001b, and\u0085sequences and update the expected output test.
ord("\v"): "\\v",
ord("\f"): "\\f",
ord("\x1b"): "\\x1b", # ESC — terminal control / escape sequences
0x85: "\\x85", # NEL (Unicode next line)
|
Remediation-routine disposition: Ran the per-PR terminal-state check on head
Next step is yours: final human review + merge approval to protected Generated by Claude Code |
StructuredFormatter only prepended an "Exception Details:" header and never stripped line separators, so any exc_info=True / logger.exception(...) sink — and structured `extra` fields the format references — could forge, corrupt, or split downstream log lines with attacker-controlled separators, even where the message itself was sanitized inline. This central fix escapes every str.splitlines() boundary (LF, CR, VT, FF, FS, GS, RS, NEL, LS, PS) plus ESC in the FINAL rendered record, covering every sink at once without touching call sites. Escapes are emitted as JSON-valid \uXXXX sequences so JSON logging (enable_json_logging=True) stays parseable, and backslash is escaped first so the transform is unambiguous and reversible. Adds tests/unit/test_logging_config_crlf.py asserting against rendered handler output (message, exc_info traceback, logger.exception, extra fields), JSON-log parseability, reversibility, and full splitlines-boundary coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFJ9na5UBaskoD4eAe1fhq
3f733f5 to
b393c8f
Compare
|
Thanks @copilot — all three findings were valid and are fixed on head
The unrelated perf commit that was previously carried here has been dropped — that Generated by Claude Code |
|
Daily-control containment: returned this PR to draft at its exact current head. Focused issue #1271 is linked, but ready state preceded reconciliation of the required active-agent receipt, exact-head independent review, and deployment evidence. No code or branch was discarded. |
CWE-117 cluster consolidation — 6 competing PRs, empirically comparedSix open PRs implement CR/LF log-forging neutralization, five of them touching the same two files ( I loaded
Findings
Proposed resolution
Net: 6 PRs to 3, with strictly greater coverage than any of the 6 has today. Reproductionprobe = [0x00,0x0A,0x0B,0x0C,0x0D,0x1B,0x1C,0x1D,0x1E,0x85,0x2028,0x2029]
fmt = StructuredFormatter("%(message)s")
for cp in probe:
rec = logging.LogRecord("t", logging.INFO, "p", 1, "x=%s", (chr(cp),), None)
print(hex(cp), chr(cp) not in fmt.format(rec)) # True == neutralized |
…verdicts The agent-completion truth gate blocks ~47 of the 69 open PRs with a bare `invalid_payload` and no remediation path. Root cause: `agentTaskApplicable()` in pr-checks.yml classifies any branch matching /^(?:agent|claude|codex|copilot|jules)[\/-]/ as agent work, so human-authored Claude Code worktree branches are held to the full AgentTask provenance contract. With no linked AgentTask issue, the collector emits `policy.agent_login` and `policy.run_id` as null and records the real reasons in `collection_errors` (missing_linked_issue, missing_agent_login, missing_agent_run_id). `evaluate()` then returned at the schema check and discarded `collection_errors` entirely -- they are only read further down, after the early return. Authors saw `invalid_payload` and nothing else. This keeps the gate fail-closed and byte-identical in `verdict` and `reasons`, and only adds `details.collection_errors` so the gate says what to fix. Verified: 112 passed against the reproduced PR #1270 payload. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…verdicts (#1331) * fix(ci): surface collection errors behind invalid_payload truth-gate verdicts The agent-completion truth gate blocks ~47 of the 69 open PRs with a bare `invalid_payload` and no remediation path. Root cause: `agentTaskApplicable()` in pr-checks.yml classifies any branch matching /^(?:agent|claude|codex|copilot|jules)[\/-]/ as agent work, so human-authored Claude Code worktree branches are held to the full AgentTask provenance contract. With no linked AgentTask issue, the collector emits `policy.agent_login` and `policy.run_id` as null and records the real reasons in `collection_errors` (missing_linked_issue, missing_agent_login, missing_agent_run_id). `evaluate()` then returned at the schema check and discarded `collection_errors` entirely -- they are only read further down, after the early return. Authors saw `invalid_payload` and nothing else. This keeps the gate fail-closed and byte-identical in `verdict` and `reasons`, and only adds `details.collection_errors` so the gate says what to fix. Verified: 112 passed against the reproduced PR #1270 payload. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(ci): route every invalid_payload verdict through a shared builder The collection_errors diagnostic was only attached to the late field-validation return. The three early invalid_payload short-circuits (payload not a dict, policy not a dict, missing/invalid policy.applicable) returned bare verdicts, so a malformed payload that never reaches field validation stayed just as opaque despite the collector having already recorded why — exactly the case the review thread raised (evaluate({"policy": {}, "collection_errors": [...]})). Extract _invalid_payload(payload, invalid_fields) and route all four invalid_payload returns through it so the diagnostic is applied consistently. verdict and reasons stay byte-identical for every input; only details is enriched, and only when the collector recorded errors — the gate remains fail-closed. Add regression coverage for both early paths and confirm a non-dict payload still returns an empty details. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011L6jqdhrKTYLinTnJYKEg9 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
The CWE-117 formatter hardening merged via #1270 (now on main) covers the str.splitlines() boundary set + ESC/FS/GS/RS but omits NUL (0x00). The only cluster PR that carried NUL, #1255, was closed during consolidation without the codepoint being carried over — so main's StructuredFormatter lets a raw NUL reach the sink, where a C-based log shipper can truncate the record. Add ord("\x00"): "\\u0000" to _UNSAFE_LOG_CHARS (the exact fix the cluster consolidation analysis on #1270 specified) so NUL is escaped to a JSON-valid, reversible sequence like the other separators. Adds a focused regression test; the existing table-driven tests auto-extend to cover it. Focused suite: 10 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GSFzJYm5bYMuoAo9ssoiAM
…1270/#1255) (#1422) fix(security): neutralize NUL (0x00) in log records (CWE-117 gap) The CWE-117 formatter hardening merged via #1270 (now on main) covers the str.splitlines() boundary set + ESC/FS/GS/RS but omits NUL (0x00). The only cluster PR that carried NUL, #1255, was closed during consolidation without the codepoint being carried over — so main's StructuredFormatter lets a raw NUL reach the sink, where a C-based log shipper can truncate the record. Add ord("\x00"): "\\u0000" to _UNSAFE_LOG_CHARS (the exact fix the cluster consolidation analysis on #1270 specified) so NUL is escaped to a JSON-valid, reversible sequence like the other separators. Adds a focused regression test; the existing table-driven tests auto-extend to cover it. Focused suite: 10 passed. Claude-Session: https://claude.ai/code/session_01GSFzJYm5bYMuoAo9ssoiAM Co-authored-by: Claude <noreply@anthropic.com>
…forgery) (#1439) fix(security): build JSON log records with json.dumps (CWE-117 #1429) The #1270 hardening escapes every line/record separator but deliberately not the double-quote, because `_UNSAFE_LOG_CHARS` is applied to a fully rendered record where escaping `"` would destroy the JSON skeleton. With `enable_json_logging` on, records were printf-interpolated into a JSON template, so a `"` in attacker content closed the `message` field and opened arbitrary new ones: log.info('benign", "level": "DEBUG", "forged": "yes') emits at INFO and parses as DEBUG, with an injected `forged` field. Most parsers take the last value on a duplicate key, so an attacker can downgrade their own entries below an alerting threshold. Escaping cannot be fixed in place: at that point attacker content and the template's structural quotes are the same characters. The fix is ordering. `StructuredFormatter` gains `json_output`; when set, it builds a dict and serializes with `json.dumps`, so escaping happens per value before any structural quote exists. `ensure_ascii=True` covers every separator the table did, including NEL/LS/PS, so records stay one physical line. The line-oriented path is untouched and keeps using the escape table. The module comment no longer claims that table makes JSON safe. Non-vacuous: removing only `"json_output": enable_json_logging` from the dictConfig fails test_setup_logging_wires_json_output_to_the_formatter and nothing else — that wiring is what a future edit could silently drop. Verified: 19 passed in tests/unit/test_logging_config_crlf.py (9 pre-existing unchanged); ruff clean on the changed files, with the repo's 2 pre-existing findings unchanged. Closes #1429 Claude-Session: https://claude.ai/code/session_01MsrR4ngeBsCT9qiBftEWbB Co-authored-by: Claude <noreply@anthropic.com>
Canonical issue
Closes #1271.
The central formatter fix that #810's confirmed-but-unmitigated review thread
(#810 discussion)
identified as living outside #810's
router.pyscope.Outcome
Every rendered log record now emits as a single physical line. Attacker-controlled
line separators can no longer forge, corrupt, or split downstream log entries —
including via
logger.error(..., exc_info=True),logger.exception(...), andstructured
extrafields — not just the inline-sanitized message string. JSONlogs stay parseable.
Scope
src/youtube_extension/backend/config/logging_config.py—StructuredFormatter.format()escapes every
str.splitlines()boundary (LF, CR, VT, FF, FS, GS, RS, NEL, LS, PS)plus ESC in the final rendered record, via a shared
sanitize_log_record()/_UNSAFE_LOG_CHARStable. Escapes are emitted asJSON-valid
\uXXXXsequences; backslash is escaped first so the transform isunambiguous and reversible.
tests/unit/test_logging_config_crlf.py— regression tests asserting againstrendered handler output (message,
exc_infotraceback,logger.exception,extrafields), JSON-log parseability, reversibility, full splitlines-boundarycoverage, and a benign-record no-op.
Single-purpose: this PR contains only the CWE-117 formatter hardening. (An earlier
revision carried an unrelated perf commit; that work has its own canonical home in
#1269 and has been dropped from this branch.)
Risk
as
\uXXXXescapes. Content is fully preserved and losslessly reversible; JSONlogging is improved (a raw newline or a non-JSON escape in
%(message)swouldpreviously have broken JSON parsing).
Verification
Tied to head
b393c8f.pytest tests/unit/test_logging_config_crlf.py→ 9 passedlint-python, lint-frontend, guards, bandit, npm-audit, python-safety, gitleaks,
dependency-review, trivy, "Canonical issue and evidence"); re-running on this head.
Production evidence
Not applicable to runtime behavior: this is a Python-only logging change with no
web surface. The
apps/webVercel preview does not exercise it. Correctness isproven by the rendered-output regression suite above rather than a deployment.
Agent provenance
Human-authored (repo owner's account) with Claude Code assistance, per the
Co-Authored-Bytrailer on the commit. Kept as an owner PR without an agent-lockmanifest, consistent with the repo's convention for owner-authored changes.
Awaiting final human review and merge approval to protected
main.