Repository navigation
Run no script from a replayed trace, in any player mode - #527
Merged
Merged
Conversation
A trace is untrusted input: whoever recorded it chose the DOM the player replays. The hosted player's CSP stopped that DOM running script, but the dashboard and show-trace sent no CSP, and the replay applied mutations unfiltered, including added <script> nodes and on* attributes. Three layers now, any one of which stops it: - the replay iframe is sandboxed to allow-same-origin, so nothing in it runs while the app can still write the replayed DOM; - replay-safety.ts drops <script>, on* attributes, srcdoc, http-equiv and javascript: URLs on the first render and on every mutation alike; - the CSP lives once in shared/player-csp.ts, the backend sends it on its HTML with the live dashboard's socket, and a test holds vercel.json equal to it. The hosted e2e check opens a hostile trace that tries each path and fails if any script runs or the frame is not sandboxed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9 tasks
Starting a replayed page puts a <base> tag first in its head, and the code spread head.props.children as an array. Preact stores one child as the child itself and none as undefined, so a page whose head had zero or one element crashed the replay with "props.children is not iterable", and a document with no head lost its <base>, its relative stylesheets then resolving against the player. The hosted check's hostile trace, whose head is empty, found it. withBaseUrl reads both through toChildArray. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What & why
Part of #494. Stacked on #523.
A trace is untrusted input: whoever recorded it chose the DOM the player replays, and the hosted player invites opening traces from other people. The hosted player's CSP already stopped a replayed page running script, but
show-traceand the live dashboard sent no CSP, and the replay applied later DOM updates without filtering them. This makes a replayed trace run no script in every mode, with three independent layers.Type of change
Packages touched
shared(types and contracts)core(framework-agnostic capture/reporting)elements(published element/snapshot API —@wdio/elements)service(WebdriverIO adapter)nightwatch-devtools(Nightwatch adapter)selenium-devtools(Selenium adapter)selenium-devtools-py(Selenium Python adapter)backend(server)app(UI)script(page-injected runtime)trace(Trace mode)Plus
conformance(the hosted e2e check).Capability matrix
packages/conformance/src/features.tsupdated, and tables regenerated withpnpm docs:capabilities, if this adds or changes user-facing behaviourNot needed: no per-adapter capability changes.
Notes for reviewers
Layers
<iframe sandbox="allow-same-origin">: the app still writes the replayed DOM, nothing inside runs, and nested frames inherit the sandboxcomponents/browser/replay-safety.tsdrops<script>,on*attributes,srcdoc,http-equivandjavascript:URLs, on the first render and on every replayed mutation (previously only the first render dropped scripts and handlers)shared/player-csp.tsis the policy. The backend sends it on its HTML responses, with the live dashboard's WebSocket source taken from theHostheader only when it is a plain host;vercel.jsonsends it for the hosted player, and a test holds the two equalshow-traceand live gain what hosted hadReplay crash found by the hostile trace: starting a replayed page puts a
<base>first in its head, and the code spreadhead.props.childrenas an array. Preact stores one child as the child itself and none asundefined, so a page whose head had zero or one element crashed the replay (props.children is not iterable), and a document with no head lost its<base>.withBaseUrlinvnode-transform.tsreads both throughtoChildArray, with tests for an empty head, one child, several, and no head.frame-ancestors 'none'now also applies to the local dashboard, so a website cannot framelocalhostand lure clicks on Rerun. Nothing in the repo embeds the dashboard in a frame.Testing
pnpm install --frozen-lockfile,pnpm build,pnpm test(3526 passed) andpnpm lintare green; the built app carries the sandbox attribute.<script>,javascript:andsrcdoc, the CSP builder and Host handling, the backend header (a malformedHostreflects nothing, API responses carry no CSP), andvercel.jsonequal to the shared policy. Removing the script filter or the backend header makes them fail.<script>. It fails unless the page was replayed, the frame is sandboxed, no<script>reached the frame, and nothing ran. This PR'shosted-playerjob is its first run.Needs checking by eye (the sandbox changes how the replay frame runs, and the backend sends a CSP for the first time)
pnpm show-trace <zip>: the replay renders styled; scrolling, the element highlight and the A11y overlay work.pnpm player:hosted: the same.pnpm demo:wdio:mocha(live): the dashboard connects and updates, with no CSP error in the console.Not in this PR: the remaining ways a trace touches the outside world are tracked: replay loading the recorded site's own stylesheets and images (#499), and redacting what a trace records (#503).
Screenshots / recordings
Not applicable: no visual change intended.
🤖 Generated with Claude Code