[OP-19618] Consolidate CSP script-nonce enforcement#24033
Open
myabc wants to merge 7 commits into
Open
Conversation
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.
479b08d to
b660548
Compare
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.
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.
bed54f7 to
b8341c0
Compare
Contributor
There was a problem hiding this comment.
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-noncehelpers (readScriptNonce,scrubScriptElements) and route Turbo fetch + render hooks through them; remove the duplicated helper fromhelpers.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
targetfrom the event shadows thetargetparameter, anddialog:closeis dispatched on the globaldocumentinstead 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')!;
|
Caution The provided work package version does not match the core version Details:
Please make sure that:
|
b660548 to
a6d0e55
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket
https://community.openproject.org/wp/OP-19618
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-Noncerequest header and a render-hook scrub helper), the scrub policy lived inside an unrelatedTurboHelpersnamespace 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:
Add a CSP script-nonce module. New
frontend/src/turbo/csp-script-nonce.tsexportsreadScriptNonce(source)andscrubScriptElements(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.Wire CSP nonce enforcement to module. Routes the
X-Turbo-Nonceheader and all three script-scrub render hooks (turbo:before-render,turbo:before-stream-render,turbo:before-frame-render) inturbo-event-listeners.tsthrough the new module, and removes the now-duplicatescrubScriptElementsfromhelpers.ts. Every scrub now reads the nonce fromtargetrather than the hardcoded globaldocument— 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.warnwasn't mocked in one spec file).Behaviour is unchanged except for the
target-vs-documentcorrectness fix noted above — this is a consolidation and coverage pass, not a policy change.Merge checklist