Skip to content

Inventory Trusted Types violations in system tests - #3194

Merged
jeremy merged 4 commits into
mainfrom
security/trusted-types-ci-inventory
Oct 15, 2026
Merged

jeremy merged 4 commits into
mainfrom
security/trusted-types-ci-inventory

Conversation

@jeremy

@jeremy jeremy commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Inventory Trusted Types violations in system tests

Before anything about Trusted Types goes to production, this lists the DOM sinks fizzy and its libraries write raw strings to, using only the system tests. Nothing is enforced, no test fails over a violation, and no report leaves the CI box.

This is track 1 of the Security Hardening card's Trusted Types work. #3092 is reworked and stacked on it as track 2: a sampled report-only pilot in production, which waits on a Trust & Compliance review.

What changes

  • TrustedTypesReportOnly middleware (config/initializers/content_security_policy.rb), carried over from Trusted Types report-only pilot: 1% of SaaS page loads, collected first-party #3092. It sends require-trusted-types-for 'script' on its own Content-Security-Policy-Report-Only header, so the app policy is unchanged byte for byte. It stacks as a second header field when the app policy is itself report-only. It's on only when config.x.content_security_policy.trusted_types_report_only is set, and only config/environments/test.rb sets it. Here it carries no report-uri, so an inherited CSP_REPORT_URI can't post test violations anywhere.
  • TrustedTypesViolationsTestHelper (test/test_helpers/trusted_types_violations_test_helper.rb), included in ApplicationSystemTestCase:
    • Before any page script runs, a listener injected with CDP Page.addScriptToEvaluateOnNewDocument records each Trusted Types securitypolicyviolation (sample, source file:line:col, page path) in sessionStorage, which survives full page loads.
    • before_teardown drains it into tmp/trusted_types_violations.jsonl, inside the window the test started in. It has to be before_teardown: teardown callbacks run after Capybara has reset the session to a blank page.
    • Minitest.after_run groups the violations by sink and by source (origin and asset digest stripped) into tmp/trusted_types_violations.json.
    • If that window is gone or a WebDriver call fails while draining, it warns and drops that test's records instead of failing the test.
    • What it can't see, so the inventory is a floor: windows other than the one a test starts in (the notification smoke test opens one), named Capybara sessions, cross-origin frames, records left in another origin's sessionStorage, and writes after the final drain.
  • CI uploads the JSON as an artifact (trusted-types-violations-{oss,saas}-{sqlite,mysql}) from each test job (actions/upload-artifact v7.0.2, pinned by SHA).

Inventory: system tests (OSS, SQLite, Chrome 152 headless, thelio, 9407b10)

40 runs, 0 failures. 3,164 violations at 21 call sites. All four CI jobs at that head (OSS and SaaS, SQLite and MySQL) agree: 21 sites, 3,149 to 3,167 violations. Re-run at 6fa278f: 3,152 violations, the same 21 sites.

Sink Violations
Element innerHTML 2,572
DOMParser parseFromString 539
ServiceWorkerContainer register (TrustedScriptURL) 37
HTMLScriptElement src (TrustedScriptURL) 15
Element insertAdjacentHTML 1

Top sources:

Count Sink Source Whose
1,002 innerHTML controllers/local_time_controller.js:93 fizzy
806 innerHTML lexxy.js:20960 lexxy
431 parseFromString turbo.min.js:5:2806 (Drive visits, frame loads) Turbo
186 innerHTML lexxy.js:8056 lexxy
123 innerHTML lexxy.js:9153 lexxy
108 parseFromString lexxy.js:8063 lexxy
65 innerHTML turbo.min.js:5:11182 (stream render) Turbo
37 serviceWorker.register turbo.min.js:19:32785 (Turbo's service worker registration) Turbo
18 + 18 innerHTML controllers/syntax_highlight_controller.js:33,34 fizzy
20 innerHTML mermaid.tiny.js mermaid
15 script src helpers/mermaid_helpers.js:77 fizzy
4 innerHTML turbo.min.js:19:44067 (stream update clearing its target) Turbo
1 insertAdjacentHTML inline the test itself (activity_pagination_test.rb:27)

The other lexxy sites account for 22 to 62 each: toolbar buttons, table controls and icons. Turbo shows up at four sites (Drive and frame parseFromString, stream-render innerHTML, stream update, service-worker registration): 537 violations. That's not all of Turbo's sinks. Its data-turbo-submits-with text swap and missing-frame rendering also write raw strings, and no system test reaches them. Either way, enforcement can't come before Turbo has a policy hook, whatever fizzy's own code does. The first-party sinks are local_time_controller, syntax_highlight_controller and mermaid_helpers.

The system suite has only 9 test files, so this is a floor, not a census. Paths no system test visits don't appear. The artifact lists the pages and tests behind each site.

Verification

  • test/middleware/trusted_types_report_only_test.rb runs the middleware under Rack::Lint: sole policy, stacking beside an existing report-only policy, non-HTML untouched.
  • test/integration/static_csp_test.rb: the enforcing policy carries no Trusted Types directive, and the report-only header carries exactly require-trusted-types-for 'script'.
  • System tests on thelio at 9407b10 (PARALLEL_WORKERS=1 bin/rails test:system, OSS/SQLite): 40 runs, 0 failures. The inventory above came from that run.
  • The first push (c3401a9) wrote no inventory. Draining in teardown read a blank page. A scratch test confirmed the listener itself recorded violations, which pointed at the drain. 9407b10 moves it to before_teardown.
  • GitHub CI uploads one artifact per test job (4 of 4 at 9407b10).
  • A scratch test that switches to a second window and ends there passes, and its first window's violations are recorded. Before 6fa278f, the same test errored in teardown (codex's repro).
  • Unregistering the listener is in an ensure, so it still happens when draining fails. A listener left registered survives Capybara's reset and double-counts every later test. Scratch pair on thelio at 3739578: a test that leaves an unhandled alert open warns and passes, and the next test records exactly 1 of 1 writes.

Risk and rollback

  • Test environment and CI only. Production, staging and beta never insert the middleware, because nothing outside test.rb sets the flag.
  • System tests make two CDP calls each and one execute_script. If Chrome stops supporting CDP from Selenium, setup raises and system tests error loudly rather than silently losing the inventory.
  • Rollback: revert. Nothing persists outside tmp/.

What it unblocks

Serve a report-only require-trusted-types-for 'script' policy in the test
environment, on its own header so the app policy is untouched. A listener
injected over CDP records each violation into sessionStorage; teardown
drains it, and the run groups violations by sink and source location into
tmp/trusted_types_violations.json, which CI uploads as an artifact.

No test fails over a violation: this inventories the DOM sinks to migrate
before any Trusted Types enforcement is designed.
Copilot AI balanced review requested due to automatic review settings October 15, 2026 22:08
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T22:42:29.663462Z 3739578 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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.

🟡 Changes recommended

Configured endpoints can send violations outside CI, and listener cleanup failures can turn inventory collection into test errors.

2 open findings
What changed in this PR

Adds a system-test inventory of Trusted Types violations to inform future enforcement without changing the enforcing policy.

Changes:

  • Enables separate report-only middleware in tests.
  • Collects violations and groups them by sink and source.
  • Tests header behavior and uploads CI inventories.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
test/​test_helpers/​trusted_types_violations_test_helper.rb Captures violations and writes grouped inventories.
test/​middleware/​trusted_types_report_only_test.rb Tests header stacking and response filtering.
test/​integration/​static_csp_test.rb Checks separation from the enforcing policy.
test/​application_system_test_case.rb Includes inventory collection.
config/​initializers/​content_security_policy.rb Adds optional report-only middleware.
config/​environments/​test.rb Enables middleware for tests.
.github/​workflows/​test.yml Uploads inventory artifacts.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread config/initializers/content_security_policy.rb Outdated
Comment thread test/test_helpers/trusted_types_violations_test_helper.rb Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9407b10071

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread test/test_helpers/trusted_types_violations_test_helper.rb
Comment thread test/test_helpers/trusted_types_violations_test_helper.rb Outdated
… them off the network

Drain and unregister the listener inside the window the test started in, so a
test that switches windows neither errors in teardown nor loses that window's
records; a vanished window warns rather than failing the test. Send the
inventory policy without a report URI, so an inherited CSP_REPORT_URI can't
post test violations anywhere, and write the inventory even when it's empty.
A listener left registered survives Capybara's reset, so every later test
would record each violation twice.
@jeremy
jeremy requested a balanced review from Copilot October 15, 2026 22:39
@jeremy

jeremy commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 3739578d2d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

🟢 Approved

The supplied review evidence shows test-scoped collection, unchanged production enforcement, and no unresolved blocking findings.

0 open findings

2 resolved since last review

🧠 Review effort: Balanced

@jeremy

jeremy commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Review summary at 3739578. CI is green on all four test jobs, and each uploads its inventory artifact. Codex found no major issues on this head, and Copilot approved it. No open threads.

Declined, with reasoning:

  • Installing the listener in every window (thread). Teardown now drains the window the test started in. New windows would need CDP target auto-attach for one call site, and that site is already covered elsewhere. It's listed under "What it can't see".
  • Overlapping test runs in one checkout (our own adversarial review, round 1). They clobber the inventory, but they already collide on the test database and tmp/. Sequential runs reset correctly.

Holding: not merged, and auto-merge isn't armed. #3092 is stacked on this branch.

@jeremy
jeremy merged commit 27d0d2e into main Oct 15, 2026
14 checks passed
@jeremy
jeremy deleted the security/trusted-types-ci-inventory branch October 15, 2026 23:23
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.

2 participants