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:
2026-07-12 17:27:22 -07:00
co-authored by Claude Opus 4.8
parent 36c3b2f503
commit e5f30a4a56
4 changed files with 143 additions and 4 deletions
+3 -3
View File
@@ -1,6 +1,6 @@
import { componentResolver } from '../components/resolver';
import { cssPropsToString } from './style-helpers';
import { escapeHtml } from './escape';
import { escapeHtml, escapeAttr } from './escape';
export interface ExportOptions {
title?: string;
@@ -24,9 +24,9 @@ function buildDataAttrs(props: Record<string, any>): string {
if (props.hideOnTablet) attrs += ' data-hide-tablet';
if (props.hideOnMobile) attrs += ' data-hide-mobile';
if (props.animation && props.animation !== 'none') {
attrs += ` data-animation="${props.animation}"`;
attrs += ` data-animation="${escapeAttr(String(props.animation))}"`;
if (props.animationDelay && props.animationDelay !== '0') {
attrs += ` data-animation-delay="${props.animationDelay}"`;
attrs += ` data-animation-delay="${escapeAttr(String(props.animationDelay))}"`;
}
}
return attrs;