Files
site-builder/craft/src/utils/html-export.test.ts
T

60 lines
2.5 KiB
TypeScript
Raw Normal View History

import { describe, test, expect } from 'vitest';
import { exportBodyHtml } from './html-export';
/**
* C2: buildDataAttrs (internal to html-export.ts) previously interpolated
* `props.animation` / `props.animationDelay` directly into
* `data-animation="${...}"` / `data-animation-delay="${...}"` with no
* escaping. Runs for EVERY node (via renderNode -> buildDataAttrs ->
* injectAttrs) and is reachable via AI `update_props` + deserialized saved
* state. A malicious value like `x"><img src=y onerror=alert(1)>` breaks
* out of the attribute and injects a live element.
*/
describe('buildDataAttrs escapes animation/animationDelay (C2)', () => {
const makeState = (props: Record<string, unknown>) =>
JSON.stringify({
ROOT: {
type: { resolvedName: 'Container' },
isCanvas: true,
props: { tag: 'div', style: {}, ...props },
displayName: 'Container',
custom: {},
hidden: false,
nodes: [],
linkedNodes: {},
},
});
test('malicious animation value is escaped -- no raw breakout, no injected <img>', () => {
const payload = 'x"><img src=y onerror=alert(1)>';
const state = makeState({ animation: payload });
const { html } = exportBodyHtml(state);
expect(html).not.toContain('<img src=y onerror=alert(1)>');
expect(html).not.toMatch(/data-animation="x">/);
// The attribute value must be escaped, so no raw `"` or `<` survives
// inside the data-animation attribute region.
expect(html).toContain('data-animation="x&quot;&gt;&lt;img src=y onerror=alert(1)&gt;"');
});
test('malicious animationDelay value is escaped -- no raw breakout', () => {
const payload = 'x"><script>alert(1)</script>';
const state = makeState({ animation: 'fade-in', animationDelay: payload });
const { html } = exportBodyHtml(state);
expect(html).not.toContain('<script>alert(1)</script>');
expect(html).toContain('data-animation-delay="x&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;"');
});
test('normal animation value still emits data-animation="fade-in" unchanged', () => {
const state = makeState({ animation: 'fade-in' });
const { html } = exportBodyHtml(state);
expect(html).toContain('data-animation="fade-in"');
});
test('normal animationDelay value still emits unchanged', () => {
const state = makeState({ animation: 'fade-in', animationDelay: '0.5s' });
const { html } = exportBodyHtml(state);
expect(html).toContain('data-animation-delay="0.5s"');
});
});