Skip to content

fix(security): neutralize CR/LF in rendered log records (CWE-117) - #1270

Merged
groupthinking merged 2 commits into
mainfrom
claude/determined-maxwell-j8w4lt
Aug 7, 2026
Merged

fix(security): neutralize CR/LF in rendered log records (CWE-117)#1270
groupthinking merged 2 commits into
mainfrom
claude/determined-maxwell-j8w4lt

Conversation

@groupthinking

@groupthinking groupthinking commented Aug 2, 2026

Copy link
Copy Markdown
Owner

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.py scope.

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(...), and
structured extra fields — not just the inline-sanitized message string. JSON
logs stay parseable.

Scope

  • src/youtube_extension/backend/config/logging_config.pyStructuredFormatter.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_CHARS table. Escapes are emitted as
    JSON-valid \uXXXX sequences; backslash is escaped first so the transform is
    unambiguous and reversible.
  • tests/unit/test_logging_config_crlf.py — regression tests asserting against
    rendered handler output (message, exc_info traceback, logger.exception,
    extra fields), JSON-log parseability, reversibility, full splitlines-boundary
    coverage, 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

  • Risk level: low
  • Failure mode: multi-line tracebacks now render on one line with separators shown
    as \uXXXX escapes. Content is fully preserved and losslessly reversible; JSON
    logging is improved (a raw newline or a non-JSON escape in %(message)s would
    previously have broken JSON parsing).
  • Rollback: revert this commit; the formatter returns to prepend-only behavior.

Verification

Tied to head b393c8f.

  • Focused tests — pytest tests/unit/test_logging_config_crlf.py9 passed
  • Addresses all three Copilot findings — see the summary comment below.
  • Required engineering CI green on the prior head (CodeQL, Security Scan JS,
    lint-python, lint-frontend, guards, bandit, npm-audit, python-safety, gitleaks,
    dependency-review, trivy, "Canonical issue and evidence"); re-running on this head.
  • Review threads — Copilot findings addressed.

Production evidence

Not applicable to runtime behavior: this is a Python-only logging change with no
web surface. The apps/web Vercel preview does not exercise it. Correctness is
proven 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-By trailer on the commit. Kept as an owner PR without an agent-lock
manifest, consistent with the repo's convention for owner-authored changes.
Awaiting final human review and merge approval to protected main.

@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Ready Ready Preview, v0 Aug 7, 2026 5:57pm

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 683d5fdc-1f07-442f-bcba-5947c88bc49c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the python label Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA f76669a.
Ensure 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 Files

None

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Agent Completion Truth Gate: BLOCKED

Reasons: invalid_payload

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"
}

Workflow evidence

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 \x85 are 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 \u0085 sequences 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)

Comment thread src/youtube_extension/backend/config/logging_config.py Outdated

Copy link
Copy Markdown
Owner Author

Remediation-routine disposition: HALTED(awaiting_human_governance_gate) — ready for your review, no autonomous merge.

Ran the per-PR terminal-state check on head 3f733f5:

  • Engineering surface is green. Vercel READY; CodeQL, Security Scan, lint-python/frontend, bandit, npm-audit, python-safety, gitleaks, dependency-review, trivy all pass. CodeRabbit is skipped by this repo's label config. Focused suite tests/unit/test_logging_config_crlf.py → 6 passed.
  • The only failing check is agent-completion/truth-gate/pr-1270 (invalid_payload) — red by design. As the PR body states, this routine deliberately did not fabricate a frozen pre-dispatch intent snapshot or a trusted terminal-agent-result, honoring fix(security): sanitize user-controlled values in API logs (CWE-117 log injection) #810's directive not to weaken or impersonate the gate. I am not overriding or "fixing" that gate; doing so would be the exact impersonation the gate exists to catch.

Next step is yours: final human review + merge approval to protected main. Nothing further will be pushed autonomously.


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
@groupthinking
groupthinking force-pushed the claude/determined-maxwell-j8w4lt branch from 3f733f5 to b393c8f Compare August 2, 2026 19:30

Copy link
Copy Markdown
Owner Author

Thanks @copilot — all three findings were valid and are fixed on head b393c8f:

  1. Missing separators (U+001C/1D/1E — FS/GS/RS). Added. The table is now the
    full set of str.splitlines() boundaries (LF, CR, VT, FF, FS, GS, RS, NEL, LS,
    PS) plus ESC. test_all_splitlines_boundaries_are_escaped builds its input from
    the table and asserts the result collapses to a single splitlines() element.

  2. Invalid JSON escapes (\v, \x1b, \x85). Every replacement is now a
    JSON-valid \uXXXX sequence ( , , , …) instead of a Python
    shorthand, so enable_json_logging=True records stay parseable.
    test_json_logging_output_stays_parseable renders through the module's JSON format
    and round-trips the message through json.loads.

  3. Non-reversible transform. Backslash is now escaped first
    (ord("\\"): "\\\\"), so a real newline ( ) and a literal backslash-n
    (\\n) never collide. test_encoding_is_reversible pins this.

The unrelated perf commit that was previously carried here has been dropped — that
work lives in its own canonical PR #1269.


Generated by Claude Code

@groupthinking
groupthinking marked this pull request as draft August 3, 2026 13:17

Copy link
Copy Markdown
Owner Author

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.

@groupthinking

Copy link
Copy Markdown
Owner Author

CWE-117 cluster consolidation — 6 competing PRs, empirically compared

Six open PRs implement CR/LF log-forging neutralization, five of them touching the same two files (backend/config/logging_config.py, tests/unit/test_logging_config_crlf.py): #810, #1255, #1263, #1265, #1266, #1270.

I loaded logging_config.py from each PR head and pushed every control/separator codepoint through StructuredFormatter.format(). All five parse and all five stop a basic \r\n injection. They differ only in which codepoints they cover:

PR NUL 0x00 ESC 0x1b FS/GS/RS 0x1c-1e LF/CR/VT/FF NEL/LS/PS Also fixes
#1263 yes yes
#1265 yes yes yes intelligent_cache.py (conflicting)
#1266 yes yes yes intelligent_cache.py
#1255 yes yes yes yes
#1270 yes yes yes yes
#810 n/a — call-site approach logsafe.py, api/v1/router.py, video_processing_service.py

Findings

  1. fix(security): neutralize CWE-117 log forging in StructuredFormatter #1263 has zero unique coverage. It is strictly dominated by fix(security): neutralize CR/LF in rendered log records (CWE-117 formatter gap) #1255, fix(security): neutralize CR/LF in rendered log records (CWE-117) #1265, fix(security): neutralize CR/LF log forging in StructuredFormatter (CWE-117) #1266 and fix(security): neutralize CR/LF in rendered log records (CWE-117) #1270. Nothing is lost by closing it.
  2. fix(security): neutralize CR/LF in rendered log records (CWE-117) #1265 is strictly dominated by fix(security): neutralize CR/LF in rendered log records (CWE-117) #1270 on the formatter, and it is currently CONFLICTING. Its only unique contribution is the intelligent_cache.py fix, which fix(security): neutralize CR/LF log forging in StructuredFormatter (CWE-117) #1266 also carries in a mergeable state.
  3. No single PR is complete. fix(security): neutralize CR/LF in rendered log records (CWE-117) #1270 has the widest separator coverage and the best escaping design — \uXXXX output stays valid JSON when enable_json_logging is on, and it escapes backslash first so the encoding is reversible. But fix(security): neutralize CR/LF in rendered log records (CWE-117) #1270 does not neutralize NUL (0x00), and fix(security): neutralize CR/LF in rendered log records (CWE-117 formatter gap) #1255 is the only PR that does. A NUL reaching a C-based log shipper can truncate the record.

Proposed resolution

Net: 6 PRs to 3, with strictly greater coverage than any of the 6 has today.

Reproduction

probe = [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

groupthinking added a commit that referenced this pull request Aug 4, 2026
…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>
groupthinking added a commit that referenced this pull request Aug 4, 2026
…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>
@groupthinking
groupthinking marked this pull request as ready for review August 7, 2026 17:55
@groupthinking
groupthinking enabled auto-merge (squash) August 7, 2026 17:55
@groupthinking
groupthinking merged commit 55e0e64 into main Aug 7, 2026
21 of 22 checks passed
@groupthinking
groupthinking deleted the claude/determined-maxwell-j8w4lt branch August 7, 2026 17:55
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

GRV-342

groupthinking pushed a commit that referenced this pull request Aug 7, 2026
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
groupthinking added a commit that referenced this pull request Aug 7, 2026
…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>
groupthinking added a commit that referenced this pull request Aug 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(security): StructuredFormatter does not strip CR/LF from exc_info tracebacks / extra fields (CWE-117)

3 participants