Site builder: security & data-loss hardening + unified asset picker + audit backlog #3
@@ -35,4 +35,22 @@ describe('cssPropsToString sanitizes emitted values (A5)', () => {
|
|||||||
expect(out).toContain('https://example.com/img.jpg');
|
expect(out).toContain('https://example.com/img.jpg');
|
||||||
expect(out).not.toContain('javascript:');
|
expect(out).not.toContain('javascript:');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('data-URI background image survives with its semicolon separator intact (A3)', () => {
|
||||||
|
const out = cssPropsToString({
|
||||||
|
backgroundImage: 'url(data:image/png;base64,iVBORw0KGgo=)',
|
||||||
|
} as any);
|
||||||
|
// The `;` between the MIME type and `base64` is a required part of the
|
||||||
|
// data-URI syntax -- it must not be stripped by breakout sanitization.
|
||||||
|
expect(out).toContain('data:image/png;base64,iVBORw0KGgo=');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('quote/semicolon breakout is still neutralized alongside a url()', () => {
|
||||||
|
const out = cssPropsToString({
|
||||||
|
color: 'red";background:url(x)"',
|
||||||
|
} as any);
|
||||||
|
expect(out).not.toContain('"');
|
||||||
|
const withoutEntities = out.replace(/&(?:quot|amp|#39|#96|#x[0-9a-f]+|#[0-9]+);/gi, '');
|
||||||
|
expect(withoutEntities).not.toContain(';');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,24 +6,41 @@ const camelToKebab = (str: string): string =>
|
|||||||
|
|
||||||
const URL_RE = /url\(\s*(['"]?)([\s\S]*?)\1\s*\)/gi;
|
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, '"');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes a single CSS declaration value so it can never terminate the
|
* Sanitizes a single CSS declaration value so it can never terminate the
|
||||||
* `style="..."` attribute early, inject an extra declaration via a stray
|
* `style="..."` attribute early, inject an extra declaration via a stray
|
||||||
* `;`, or smuggle a `javascript:`/`vbscript:`/`data:text/html` URL through
|
* `;`, or smuggle a `javascript:`/`vbscript:`/`data:text/html` URL through
|
||||||
* a `url(...)` reference. Legitimate multi-part values (box-shadow,
|
* a `url(...)` reference. Legitimate multi-part values (box-shadow,
|
||||||
* gradients, etc.) that contain none of these characters pass through
|
* 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 {
|
function sanitizeCssValue(raw: string): string {
|
||||||
// Neutralize url(...) references: validate/strip the scheme and re-wrap
|
let out = '';
|
||||||
// in single quotes with the contents escaped for attribute safety.
|
let lastIndex = 0;
|
||||||
let val = raw.replace(URL_RE, (_m, _q, inner) => `url('${escapeAttr(safeUrl(inner.trim()))}')`);
|
URL_RE.lastIndex = 0;
|
||||||
// A `;` in a CSS value is never legitimate (declarations are separated by
|
let m: RegExpExecArray | null;
|
||||||
// it) -- stray semicolons are how a breakout injects a second property.
|
while ((m = URL_RE.exec(raw)) !== null) {
|
||||||
val = val.replace(/;/g, '');
|
// Sanitize breakout characters only in the segment before this url(...)
|
||||||
// Any remaining raw `"` would close the `style="..."` attribute early.
|
// reference -- never inside the reference itself.
|
||||||
val = val.replace(/"/g, '"');
|
out += sanitizeBreakoutChars(raw.slice(lastIndex, m.index));
|
||||||
return val;
|
// 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 {
|
export function cssPropsToString(style: CSSProperties | undefined): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user