Skip to content

fix(report): stop over-escaping the CSP meta-tag content across all HTML reports - #1230

Closed
seonghobae wants to merge 1 commit into
mainfrom
fix/csp-meta-content-escaping-breaks-policy
Closed

seonghobae wants to merge 1 commit into
mainfrom
fix/csp-meta-content-escaping-breaks-policy

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Root cause

Strix found a MEDIUM (CVSS 6.5, CWE-693) finding while scanning unrelated,
in-progress PR #1081 (feat/cross-engine-conformance-inventory-1077): the
new report generator it added embeds its Content-Security-Policy through
escape(_CSP, quote=True) inside the meta tag's content attribute.
html.escape converts the CSP's literal 'none' source expressions to
'none', which browsers do not parse as valid CSP syntax — so the
meta-delivered defense-in-depth policy silently does nothing.

Auditing showed the exact same escape(_content_security_policy(), quote=True)
pattern already exists on main, in every standalone HTML report this
project emits:

  • python/fast_mlsirm/report.py
  • python/fast_mlsirm/scoring/essay/report_html.py,
    calibration_report_html.py, validation_report_html.py
  • scripts/build_benchmark_report.py
  • scripts/build_buyer_packet.py
  • scripts/build_commercial_release.py
  • scripts/build_figma_evidence_sync.py
  • scripts/build_pr_queue_governance.py
  • scripts/build_procurement_due_diligence.py
  • scripts/build_release_evidence_index.py

Four of the corresponding tests (test_report.py,
test_scoring_essay_report_html.py,
test_scoring_essay_validation_report_html.py,
test_scoring_essay_facets_report_html.py) actively pinned the broken,
escaped string (assert "default-src 'none'" in html) as the
expected output, so the regression was invisible to the suite.

Fix

Each _content_security_policy() implementation returns a fixed,
non-user-controlled string literal — verified individually for all 9
definitions before changing anything. Removed the escape(..., quote=True)
wrapper at all 11 call sites so the CSP is embedded verbatim. escape stays
imported and used elsewhere in every touched file (titles, table cells,
disclaimers), so no unused-import fallout.

Updated the 4 tests that pinned the escaped form to assert the literal
default-src 'none' directive and added a negative assertion
("&#x27;" not in <csp-slice>) so the policy cannot silently regress back
into an escaped, inert state.

Scope boundary

Pure-Python report-rendering fix. No change to CLI behavior, data models,
Rust core, or any other report content — only the CSP meta-tag value.

Verification

  • pytest tests/test_report.py tests/test_scoring_essay_report_html.py tests/test_scoring_essay_validation_report_html.py tests/test_scoring_essay_facets_report_html.py tests/test_benchmark_report.py tests/test_buyer_evidence_packet.py tests/test_commercial_release_builder.py tests/test_figma_evidence_sync.py tests/test_pr_queue_governance.py tests/test_procurement_due_diligence.py tests/test_release_evidence_index.py
    — 107 passed.
  • python scripts/render_changelog_fragments.py --check CHANGELOG.md passes
    with the new docs/changelog.d/csp-meta-content-escaping.md fragment.
  • pytest tests/test_changelog_fragment_contract.py — 8 passed.

Open in Devin Review

Summary by CodeRabbit

  • Bug Fixes

    • Corrected Content Security Policy formatting in generated HTML reports so browser-recognized directives remain intact.
    • Improved reliability for numerical validation, workflow retries, metadata handling, callbacks, statistical checks, and parallel-analysis safeguards.
    • Added clearer evidence requirements for item-bank suspension/reactivation and equating-control validation.
  • Tests

    • Expanded report validation to ensure CSP directives use literal policy syntax and prevent regressions.

…TML reports

Every standalone HTML report generator (python/fast_mlsirm/report.py, the
essay report/calibration/validation HTML family, and the benchmark,
buyer-packet, commercial-release, Figma-evidence-sync, PR-queue-governance,
procurement-due-diligence, and release-evidence-index builder scripts)
embedded its Content-Security-Policy through
`escape(_content_security_policy(), quote=True)` inside the meta tag's
`content` attribute. html.escape converts the CSP's literal `'none'` /
`'unsafe-inline'` source expressions to `&#x27;none&#x27;`, which browsers
do not parse as valid CSP syntax, so the meta-delivered defense-in-depth
policy was silently disabled on every emitted report while the tests
asserted the broken, escaped string as the expected output.

Found by Strix (CWE-693, CVSS 6.5) while scanning an unrelated in-progress
PR (#1081) that introduced a new copy of the same pattern; the bug already
existed in the 11 files above on main. Each `_content_security_policy()`
implementation returns a fixed, non-user-controlled string, so removing the
escape call is safe. Updated the four tests that pinned the escaped form to
assert the literal CSP directive and added a negative assertion against
re-escaping.

Verified: 107 passed across the affected report/CSP test files;
scripts/render_changelog_fragments.py --check passes with the new fragment.
@coderabbitai

coderabbitai Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 040b4a38-659c-4c01-9458-d9125f961099

📥 Commits

Reviewing files that changed from the base of the PR and between 04d0bc2 and a8ed866.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • docs/changelog.d/csp-meta-content-escaping.md
  • python/fast_mlsirm/report.py
  • python/fast_mlsirm/scoring/essay/calibration_report_html.py
  • python/fast_mlsirm/scoring/essay/report_html.py
  • python/fast_mlsirm/scoring/essay/validation_report_html.py
  • scripts/build_benchmark_report.py
  • scripts/build_buyer_packet.py
  • scripts/build_commercial_release.py
  • scripts/build_figma_evidence_sync.py
  • scripts/build_pr_queue_governance.py
  • scripts/build_procurement_due_diligence.py
  • scripts/build_release_evidence_index.py
  • tests/test_report.py
  • tests/test_scoring_essay_facets_report_html.py
  • tests/test_scoring_essay_report_html.py
  • tests/test_scoring_essay_validation_report_html.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change removes quote escaping from CSP meta-tag rendering across report generators and builder scripts. Tests now require literal CSP directives. Changelog files document the CSP correction and additional release entries.

Changes

CSP escaping correction

Layer / File(s) Summary
Direct CSP policy rendering
python/fast_mlsirm/report.py, python/fast_mlsirm/scoring/essay/*_report_html.py, scripts/build_*.py
CSP policy strings are inserted directly into generated HTML meta tags instead of applying quote escaping.
CSP regression coverage and release notes
tests/test_report.py, tests/test_scoring_essay_*_report_html.py, docs/changelog.d/csp-meta-content-escaping.md, CHANGELOG.md
Tests require literal default-src 'none' directives and reject &#x27; in CSP metadata. Changelog entries record the CSP correction and other release changes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: 🔵 Low · up to a8ed8

The PR correctly changes CSP meta-tag rendering across the HTML reports, but the changelog wording about escaped quotes disabling CSP may be technically inaccurate. The change is otherwise mergeable with owner follow-up to revise that claim or provide browser-level confirmation.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 15 files. (2 skipped: 2 unsupported.) Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: removing over-escaping from CSP meta-tag content across HTML reports.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/csp-meta-content-escaping-breaks-policy

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.

seonghobae added a commit that referenced this pull request Aug 22, 2026
…ine report

Strix flagged this exact pattern on this PR (CWE-693, CVSS 6.5):
escape(_CSP, quote=True) converts the CSP's literal 'none' source
expression to &#x27;none&#x27;, which browsers do not parse as valid CSP
syntax, silently disabling the meta-delivered policy. _CSP is a fixed
module constant with no user-controlled content, so interpolating it
directly is safe. This mirrors the same fix already applied to every
other report generator on main in #1230.

Added a regression assertion that the CSP is embedded unescaped.
Verified: pytest tests/test_cross_engine_conformance_report.py -- 8 passed.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

Copy link
Copy Markdown
Contributor Author

RCA correction: this PR is based on a false-positive browser-parsing assumption and should not land as a security fix.

HTML character references in attribute source markup are decoded by the HTML parser before the DOM/content attribute is consumed. In particular, source text such as content="default-src &#x27;none&#x27;" yields the decoded attribute value default-src 'none'; the CSP pragma therefore receives the quoted CSP source expression, not the literal &#x27; bytes. MDN documents this parsing rule explicitly: https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#decoded_character_references_in_attribute_values . CodeRabbit's current-head walkthrough independently flags the changelog claim as technically inaccurate.

The new tests assert a serialization spelling (&#x27; absent) rather than browser/DOM CSP semantics, so they do not demonstrate a CWE-693 remediation. Removing attribute escaping is unnecessary for the present fixed policy and would reduce defense-in-depth if the policy string later acquires characters significant to the surrounding double-quoted HTML attribute.

Closing unmerged. If we add CSP acceptance evidence later, it should verify the parsed DOM/policy semantics (or a browser-level enforcement fixture) rather than require one source-serialization spelling.

@seonghobae seonghobae closed this Aug 22, 2026
seonghobae added a commit that referenced this pull request Aug 24, 2026
)

* test(validation): define cross-engine conformance inventory contract

* feat(validation): add cross-engine conformance inventory

* docs(changelog): record cross-engine conformance inventory

* test(validation): cover conformance contract fail-closed edges

* fix(validation): seal conformance package records

* test(validation): seal conformance record subclasses

* fix(validation): complete conformance commit provenance boundary

* fix(validation): require executed conformance evidence

* test(validation): reproduce unsupported conformance coverage claim

* test(validation): pin execution-backed coverage invariant

* test(validation): reproduce Unicode conformance identity drift

* fix(validation): normalize conformance text before hashing

* test(validation): make schema-version regex literal

* test(conformance): expose post-init mutation replay gap

* fix(conformance): revalidate records on manifest replay

* docs(conformance): record replay integrity hardening

* test(conformance): document replay fixtures

* fix(changelog): restore authoritative fragment format

* feat(validation): add conformance run provenance (#1085)

* feat(validation): add conformance run provenance

* fix(changelog): restore fragment headings

* test(validation): reproduce run provenance replay bypass

* fix(validation): replay run provenance before serialization

* docs(validation): record provenance replay integrity

* fix(validation): bind executed conformance to run output provenance (#1093)

* test(conformance): require output provenance for executed evidence

* fix(conformance): bind executed evidence to run outputs

* test(conformance): align fixtures with executed provenance contract

* docs(changelog): record executed conformance provenance gate

* test(conformance): seal mutated output provenance before execution gate

* test(conformance): provide provenance for executed mutation fixture

* Revert "test(conformance): provide provenance for executed mutation fixture"

This reverts commit b94cf2e.

* test(conformance): provide legacy provenance fixture

* feat(validation): complete conformance runtime provenance (#1095)

* test(conformance): require runtime and redistribution provenance

* feat(conformance): bind runtime and redistribution provenance

* test(conformance): align inventory fixtures with runtime provenance

* test(conformance): carry runtime identities in provenance replay fixture

* test(conformance): carry runtime identities in execution fixture

* docs(changelog): record runtime provenance contract

* test(conformance): cover malformed runtime provenance controls

* test(conformance): provide provenance for executed mutation fixture

* feat(validation): strictly replay persisted conformance manifests (#1097)

* test(conformance): require strict persisted manifest replay

* feat(conformance): strictly replay persisted manifests

* docs(changelog): record strict conformance manifest replay

* test(conformance): cover nested manifest replay boundaries

* test(conformance): require stable JSON resource failures

* fix(conformance): stabilize bounded JSON replay failures

* fix(conformance): bound parsed manifest nesting

* test(conformance): provide provenance for executed mutation fixture

* feat(validation): render accessible cross-engine conformance evidence (#1164)

* test(validation): require accessible conformance evidence report

* feat(validation): render accessible conformance evidence

* docs(changelog): record conformance evidence report

* test(validation): require downloadable long-form conformance rows

* feat(validation): export long-form conformance evidence rows

* docs(changelog): record long-form conformance export

* fix(report): stop over-escaping the CSP meta-tag content in cross-engine report

Strix flagged this exact pattern on this PR (CWE-693, CVSS 6.5):
escape(_CSP, quote=True) converts the CSP's literal 'none' source
expression to &#x27;none&#x27;, which browsers do not parse as valid CSP
syntax, silently disabling the meta-delivered policy. _CSP is a fixed
module constant with no user-controlled content, so interpolating it
directly is safe. This mirrors the same fix already applied to every
other report generator on main in #1230.

Added a regression assertion that the CSP is embedded unescaped.
Verified: pytest tests/test_cross_engine_conformance_report.py -- 8 passed.

* Revert "fix(report): stop over-escaping the CSP meta-tag content in cross-engine report"

This reverts commit 051bc1b.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant