Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/isomorphic/trace/snapshotRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ export class SnapshotRenderer {
// crafted trace file could include them to achieve XSS.
if (name.toUpperCase() === 'SCRIPT')
return;
// Reject names that would emit extra attributes (space, =, <, >, /).
// Allow Unicode custom elements such as math-α.
if (/[\s=<>\/]/.test(name))
return;
// Element node.
// Note that <noscript> will not be rendered by default in the trace viewer, because
// JS is enabled. So rename it to <x-noscript>.
Expand Down
28 changes: 28 additions & 0 deletions tests/library/snapshot-renderer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,34 @@ test('snapshot renderer handles case-insensitive iframe tag names', () => {
expect(html).toContain('__playwright_src__');
});

test('snapshot renderer drops unsafe tag names that smuggle attributes', () => {
const renderer = new SnapshotRenderer(new LRUCache(1_000_000), [], [makeSnapshot({
html: ['HTML', {}, ['BODY', {}, ['img src=x onerror=alert(1)', {}], 'safe']],
Comment thread
SebTardif marked this conversation as resolved.

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.

can you share more info about how this could actually happen? i thought we already did a lot of sanitization that would prevent this so im concerned we're adding additional protection for something that cant actually happen

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Rousso (@dcrousso)

can you share more info about how this could actually happen? i thought we already did a lot of sanitization that would prevent this

A recorded page cannot produce this. Capture uses live node.nodeName, and document.createElement('iframe src=...') throws.

A zip opened with show-trace can. Load is JSON.parse then addFrameSnapshot with no name check (traceModernizer.ts).

The existing filters only match exact names (toUpperCase() === 'IFRAME'|'META'|'IMG'). They never see iframe src=https://evil.example, so the iframe src rewrite does not run. Unfixed render of ['iframe src=https://evil.example', {}] is <iframe src=https://evil.example>.

Same crafted-zip model as the SCRIPT / on* / srcdoc / object / embed / doctype guards already in this file.

!name is not needed. Empty name only emits <>. Dropped that check in 169e8e4eb.

})], [], 0);
const { html } = renderer.render();
expect(html).not.toContain('onerror=alert(1)');
expect(html).not.toContain('<img src=x');
expect(html).toContain('safe');
});

test('snapshot renderer still renders ordinary custom elements', () => {
const renderer = new SnapshotRenderer(new LRUCache(1_000_000), [], [makeSnapshot({
html: ['HTML', {}, ['BODY', {},
['MY-WIDGET', { 'id': 'w' }, 'hello'],
['MY-WIDGET_V2', { 'id': 'w2' }, 'v2'],
['MY-WIDGET.V2', { 'id': 'w3' }, 'dot'],
['math-α', { 'id': 'm' }, 'alpha'],
['my-élément', { 'id': 'e' }, 'elem'],
]],
})], [], 0);
const { html } = renderer.render();
expect(html).toContain('<MY-WIDGET id="w">hello</MY-WIDGET>');
expect(html).toContain('<MY-WIDGET_V2 id="w2">v2</MY-WIDGET_V2>');
expect(html).toContain('<MY-WIDGET.V2 id="w3">dot</MY-WIDGET.V2>');
expect(html).toContain('<math-α id="m">alpha</math-α>');
expect(html).toContain('<my-élément id="e">elem</my-élément>');
});

test('stripAnsiEscapes should not exhibit polynomial backtracking', () => {
// \x1b[ + 50000 semicolons + non-terminal character.
// Before the fix this took >3 seconds due to O(n^2) backtracking.
Expand Down
Loading