fix(builder): preserve data-URI semicolons in css value sanitizer

sanitizeCssValue's blanket `;` strip ran on the whole value AFTER url(...)
content was already safely re-wrapped, corrupting legitimate
data:image/png;base64,... URLs pasted into Background Image fields
(the MIME/base64 separator `;` was deleted, breaking the data URI in
exported/published HTML). Scope the `;`/`"` breakout sanitization to the
segments outside url(...) matches only -- the url() branch is already
fully safe via escapeAttr(safeUrl(...)) and must not be re-stripped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 12:15:23 -07:00
parent fb4e9f87be
commit 4c001e1af4
2 changed files with 45 additions and 10 deletions
+27 -10
View File
@@ -6,24 +6,41 @@ const camelToKebab = (str: string): string =>
const URL_RE = /url\(\s*(['"]?)([\s\S]*?)\1\s*\)/gi;
// Outside of a url(...) reference, a `;` is never legitimate (declarations
// are separated by it) -- stray semicolons are how a breakout injects a
// second property -- and a raw `"` would close the `style="..."` attribute
// early. Inside url('...') the content has already been made safe via
// escapeAttr(safeUrl(...)), including any `;` required by data-URI syntax
// (`data:<mime>;base64,<payload>`), so this must never be applied there.
const sanitizeBreakoutChars = (s: string): string => s.replace(/;/g, '').replace(/"/g, '&quot;');
/**
* Sanitizes a single CSS declaration value so it can never terminate the
* `style="..."` attribute early, inject an extra declaration via a stray
* `;`, or smuggle a `javascript:`/`vbscript:`/`data:text/html` URL through
* a `url(...)` reference. Legitimate multi-part values (box-shadow,
* gradients, etc.) that contain none of these characters pass through
* unchanged.
* unchanged, and legitimate `;`-containing data-URIs inside url(...) are
* preserved intact.
*/
function sanitizeCssValue(raw: string): string {
// Neutralize url(...) references: validate/strip the scheme and re-wrap
// in single quotes with the contents escaped for attribute safety.
let val = raw.replace(URL_RE, (_m, _q, inner) => `url('${escapeAttr(safeUrl(inner.trim()))}')`);
// A `;` in a CSS value is never legitimate (declarations are separated by
// it) -- stray semicolons are how a breakout injects a second property.
val = val.replace(/;/g, '');
// Any remaining raw `"` would close the `style="..."` attribute early.
val = val.replace(/"/g, '&quot;');
return val;
let out = '';
let lastIndex = 0;
URL_RE.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = URL_RE.exec(raw)) !== null) {
// Sanitize breakout characters only in the segment before this url(...)
// reference -- never inside the reference itself.
out += sanitizeBreakoutChars(raw.slice(lastIndex, m.index));
// Neutralize the url(...) reference: validate/strip the scheme and
// re-wrap in single quotes with the contents escaped for attribute
// safety. This is already fully safe, `;` and all.
const inner = m[2];
out += `url('${escapeAttr(safeUrl(inner.trim()))}')`;
lastIndex = URL_RE.lastIndex;
}
out += sanitizeBreakoutChars(raw.slice(lastIndex));
return out;
}
export function cssPropsToString(style: CSSProperties | undefined): string {