Skip to content

[OP-19618] Consolidate CSP script-nonce enforcement#24033

Open
myabc wants to merge 7 commits into
code-maintenance/OP-19617-turbo-test-seamfrom
code-maintenance/OP-19618-csp-nonce-consolidation
Open

[OP-19618] Consolidate CSP script-nonce enforcement#24033
myabc wants to merge 7 commits into
code-maintenance/OP-19617-turbo-test-seamfrom
code-maintenance/OP-19618-csp-nonce-consolidation

Conversation

@myabc

@myabc myabc commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Ticket

https://community.openproject.org/wp/OP-19618

Stacked on #23980 (OP-19617) — base branch is code-maintenance/OP-19617-turbo-test-seam, not dev. Merges after #23980.

What are you trying to accomplish?

CSP script-nonce enforcement is the security-critical half of the Turbo integration, and it was scattered: the nonce was read in two independent places (the X-Turbo-Nonce request header and a render-hook scrub helper), the scrub policy lived inside an unrelated TurboHelpers namespace alongside progress-bar code, and none of it had test coverage.

This PR is Part A of OP-19618: consolidating the CSP nonce read/scrub logic into one tested module. Part B (custom-element morph policy) is separate, tracked under its own follow-up.

What approach did you choose and why?

Two commits, mirroring the OP-19617 seam pattern this branch is stacked on:

  1. Add a CSP script-nonce module. New frontend/src/turbo/csp-script-nonce.ts exports readScriptNonce(source) and scrubScriptElements(fragment, nonce) — the single owner of "what is our nonce" and "which scripts violate it". Purely additive; nothing calls it yet. TDD throughout, 6 new tests.

  2. Wire CSP nonce enforcement to module. Routes the X-Turbo-Nonce header and all three script-scrub render hooks (turbo:before-render, turbo:before-stream-render, turbo:before-frame-render) in turbo-event-listeners.ts through the new module, and removes the now-duplicate scrubScriptElements from helpers.ts. Every scrub now reads the nonce from target rather than the hardcoded global document — closing a real (if narrow) correctness gap in the old helper. Adds regression coverage for all three hooks, none of which existed before.

A third commit fixes test-output noise found during final review (console.warn wasn't mocked in one spec file).

Behaviour is unchanged except for the target-vs-document correctness fix noted above — this is a consolidation and coverage pass, not a policy change.

Merge checklist

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

myabc added 4 commits June 30, 2026 21:40
The Turbo integration listeners bound to the global `document` and
returned `void`, so the most Turbo-upgrade-fragile frontend code had no
unit coverage beyond a single Selenium navigation spec.

Threads an optional event target and `AbortSignal` through the
installers so vitest can drive synthetic `turbo:*` events and tear the
listeners down between cases. Adds specs for the seamed installers and
all five custom stream actions; callers stay unchanged through the
defaults.
The frame-visit monkeypatch reached into Turbo internals behind
`@ts-nocheck` and a blanket eslint-disable, so neither the compiler nor
a test would notice a Turbo upgrade renaming the method or fixing
hotwired/turbo#1300 underneath it.

Documents the single divergence from upstream -- sourcing the history
snapshot from the live document rather than the fetch response -- pins
it to Turbo 8.0.x, and adds a tripwire spec that fails on either upgrade
so the patch is re-evaluated instead of silently breaking.
The vitest tripwire guards the patched method's upstream shape but not
its runtime effect. This adds the behavioural half on the departments
page, whose content area is a `data-turbo-action="advance"` frame:
advancing through the tree and pressing back must restore the parent
frame.

Without `turbo-navigation-patch.ts` the URL reverts while the frame
keeps the child content, so the exact breadcrumb match fails. The bug
only reproduces with Rails-rendered frames, which is why it cannot live
in the vitest suite.
The wrapper destroys and re-bootstraps the whole Angular root on every
Turbo navigation -- the most consequential frontend/src/turbo/ installer
yet the only one left without coverage.

Threads the same (target, signal) seam and injects the plugin-context
lookup and bootstrap call so vitest can drive synthetic turbo:load
events. The RxJS skip(1) pipeline and destroy-then-bootstrap contract
are unchanged; callers stay on the defaults.
@myabc myabc force-pushed the code-maintenance/OP-19617-turbo-test-seam branch from 479b08d to b660548 Compare June 30, 2026 20:41
The nonce used to scrub mismatched-nonce <script> elements was read
in two places (the X-Turbo-Nonce header and the render-hook scrub
helper), with no test covering either. Gives both a single, tested
source: readScriptNonce resolves our nonce, scrubScriptElements
applies it.

Additive only -- nothing calls this yet.
myabc added 2 commits June 30, 2026 21:44
Routes the X-Turbo-Nonce header and all three script-scrub render
hooks through the new csp-script-nonce module instead of the
document-global TurboHelpers.scrubScriptElements, and removes that
now-duplicate export. Behaviour is unchanged; adds the regression
coverage that didn't exist for any of these hooks before.
scrubScriptElements warns on every removed script, so three of
the four scrubScriptElements cases printed to stderr. Mocks and
restores console.warn around that describe block to keep test
output pristine, matching the convention already used in the
sibling describe block added for the same module's caller.
@myabc myabc force-pushed the code-maintenance/OP-19618-csp-nonce-consolidation branch from bed54f7 to b8341c0 Compare June 30, 2026 20:44

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

This PR strengthens and expands the Turbo integration’s security and testability by consolidating CSP script-nonce handling into a dedicated module, wiring Turbo event hooks through it, and adding regression/unit coverage around several Turbo behaviors (including a Rails system spec for the Turbo frame back-button workaround).

Changes:

  • Introduce csp-script-nonce helpers (readScriptNonce, scrubScriptElements) and route Turbo fetch + render hooks through them; remove the duplicated helper from helpers.ts.
  • Add/extend “test seam” support for Turbo installers (optional event target + abort signal) and introduce multiple new Vitest specs for Turbo-related behaviors.
  • Add/clarify the Turbo navigation patch documentation and add both a Vitest “tripwire” spec and a Rails system regression spec for the back-button/frame restoration behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
spec/features/admin/departments_spec.rb Adds a Rails system regression spec for the Turbo frame back-button restoration workaround.
frontend/src/turbo/turbo-navigation-patch.ts Documents and adjusts the Turbo internal patch behavior (snapshot from live document).
frontend/src/turbo/turbo-navigation-patch.spec.ts Adds a Vitest tripwire to detect upstream changes that should trigger patch re-evaluation.
frontend/src/turbo/turbo-global-listeners.ts Adds an optional (target, signal) seam to global Turbo listeners.
frontend/src/turbo/turbo-global-listeners.spec.ts Adds unit coverage for the OPCE custom-element morph guard.
frontend/src/turbo/turbo-event-listeners.ts Wires CSP nonce header + script scrubbing through the new module; adds (target, signal) seam.
frontend/src/turbo/turbo-event-listeners.spec.ts Adds unit coverage for dialog-close behavior, nonce header injection, and script-nonce scrubbing across hooks.
frontend/src/turbo/turbo-angular-wrapper.ts Adds injectable seam options for testability and signal-based teardown of the RxJS subscription.
frontend/src/turbo/turbo-angular-wrapper.spec.ts Adds unit coverage for Angular re-bootstrap behavior on Turbo navigation.
frontend/src/turbo/live-region-stream-action.spec.ts Adds unit coverage for the live-region Turbo stream action announcement behavior.
frontend/src/turbo/input-caption-stream-action.spec.ts Adds unit coverage for the input-caption Turbo stream action behavior.
frontend/src/turbo/helpers.ts Removes the now-duplicated scrubScriptElements implementation.
frontend/src/turbo/flash-stream-action.spec.ts Adds unit coverage for flash stream action append/replace behavior.
frontend/src/turbo/csp-script-nonce.ts Introduces the consolidated CSP script-nonce read/scrub module.
frontend/src/turbo/csp-script-nonce.spec.ts Adds unit coverage for nonce reading and fragment/script scrubbing behavior.
frontend/src/turbo/action-menu-morph-remount.ts Adds an optional (target, signal) seam to the action-menu morph remount hook.
frontend/src/turbo/action-menu-morph-remount.spec.ts Adds unit coverage for action-menu remount behavior on morph events.
Comments suppressed due to low confidence (1)

frontend/src/turbo/turbo-event-listeners.ts:17

  • In the turbo:submit-end handler, destructuring target from the event shadows the target parameter, and dialog:close is dispatched on the global document instead of the passed-in target. This breaks the new (target, signal) seam for non-global documents and makes the code harder to read.
  targetDocument.addEventListener('turbo:submit-end', (event) => {
    const { detail: { success }, target:eventTarget } = event;

    if (success && eventTarget instanceof HTMLFormElement) {
      const dialog = eventTarget.closest('dialog')!;

@myabc myabc marked this pull request as ready for review June 30, 2026 20:55
@github-actions

Copy link
Copy Markdown

Caution

The provided work package version does not match the core version

Details:

Please make sure that:

  • The work package version OR your pull request target branch is correct

@myabc myabc force-pushed the code-maintenance/OP-19617-turbo-test-seam branch from b660548 to a6d0e55 Compare July 1, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants