Fix C1/C2: XSS via unsanitized style keys and animation attrs
C1: cssPropsToString emitted the camelToKebab'd style-object KEY unsanitized while only sanitizing the VALUE. A malicious style key containing a quote (reachable via AI update_props or deserialized saved state) could close the style="..." attribute and inject a live element. Now validates each key against a CSS property/custom-prop allowlist and drops anything that doesn't match. C2: buildDataAttrs (html-export.ts) interpolated props.animation and props.animationDelay directly into data-animation="..."/ data-animation-delay="..." with no escaping, for every exported node. Now routes both through escapeAttr. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,3 +54,69 @@ describe('cssPropsToString sanitizes emitted values (A5)', () => {
|
||||
expect(withoutEntities).not.toContain(';');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cssPropsToString sanitizes emitted KEYS (C1 -- style-object key breakout)', () => {
|
||||
test('a key containing a double-quote is dropped entirely, not emitted', () => {
|
||||
const out = cssPropsToString({
|
||||
color: 'red',
|
||||
'"><img src=x onerror=alert(1)>': '1',
|
||||
} as any);
|
||||
expect(out).not.toContain('"><img');
|
||||
expect(out).not.toContain('onerror');
|
||||
expect(out).toBe('color:red');
|
||||
});
|
||||
|
||||
test('a key containing > is dropped', () => {
|
||||
const out = cssPropsToString({ 'foo>bar': '1' } as any);
|
||||
expect(out).toBe('');
|
||||
});
|
||||
|
||||
test('a key containing < is dropped', () => {
|
||||
const out = cssPropsToString({ 'foo<bar': '1' } as any);
|
||||
expect(out).toBe('');
|
||||
});
|
||||
|
||||
test('a key containing a semicolon is dropped', () => {
|
||||
const out = cssPropsToString({ 'foo;bar': '1' } as any);
|
||||
expect(out).toBe('');
|
||||
});
|
||||
|
||||
test('a key containing a space is dropped', () => {
|
||||
const out = cssPropsToString({ 'foo bar': '1' } as any);
|
||||
expect(out).toBe('');
|
||||
});
|
||||
|
||||
test('legit simple key "color" is preserved', () => {
|
||||
const out = cssPropsToString({ color: 'red' } as any);
|
||||
expect(out).toBe('color:red');
|
||||
});
|
||||
|
||||
test('legit camelCase key backgroundColor -> background-color is preserved', () => {
|
||||
const out = cssPropsToString({ backgroundColor: 'blue' } as any);
|
||||
expect(out).toBe('background-color:blue');
|
||||
});
|
||||
|
||||
test('a CSS custom property key (--custom-prop) is preserved', () => {
|
||||
const out = cssPropsToString({ '--custom-prop': '10px' } as any);
|
||||
expect(out).toBe('--custom-prop:10px');
|
||||
});
|
||||
|
||||
test('a vendor-prefixed key (-webkit-...) is preserved', () => {
|
||||
const out = cssPropsToString({ WebkitBoxShadow: '0 0 1px red' } as any);
|
||||
expect(out).toBe('-webkit-box-shadow:0 0 1px red');
|
||||
});
|
||||
|
||||
test('a digits-only key is dropped', () => {
|
||||
const out = cssPropsToString({ '123': '1' } as any);
|
||||
expect(out).toBe('');
|
||||
});
|
||||
|
||||
test('other legit keys in the same object still emit even when a malicious key is dropped', () => {
|
||||
const out = cssPropsToString({
|
||||
color: 'red',
|
||||
'"><script>alert(1)</script>': 'x',
|
||||
backgroundColor: 'blue',
|
||||
} as any);
|
||||
expect(out).toBe('color:red;background-color:blue');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user