mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 20:17:09 +00:00
fix(web-runtime): use CSS.escape and validate tag-name selectors
The previous cssEscape only handled " and \, leaving newlines/control chars to break out of attribute string literals. Delegate to the platform CSS.escape per CSSOM spec. The `tag` selector branch returned the bare value through cssEscape, which doesn't prevent pseudo-classes (`*:hover`) from injecting into the surrounding selector. Add a positive whitelist; values that don't match a tag-name pattern collapse to a never-matching `:not(*)`. Also switch class selectors to `[class~="..."]` to remove the only identifier-context use of cssEscape.
This commit is contained in:
1 parent
d027bf304d
commit
8c7470a1a6
1 file changed
+18
-5
@@ -48,10 +48,9 @@ const KNOWN_KEY_TO_CSS: Record<string, (value: string) => string> = {
|
|||||||
testTag: (v) => `[data-testid="${cssEscape(v)}"]`,
|
testTag: (v) => `[data-testid="${cssEscape(v)}"]`,
|
||||||
testID: (v) => `[data-testid="${cssEscape(v)}"]`,
|
testID: (v) => `[data-testid="${cssEscape(v)}"]`,
|
||||||
"data-testid": (v) => `[data-testid="${cssEscape(v)}"]`,
|
"data-testid": (v) => `[data-testid="${cssEscape(v)}"]`,
|
||||||
className: (v) => `.${cssEscape(v)}`,
|
className: (v) => `[class~="${cssEscape(v)}"]`,
|
||||||
class: (v) => `.${cssEscape(v)}`,
|
class: (v) => `[class~="${cssEscape(v)}"]`,
|
||||||
tag: (v) => cssEscape(v),
|
tag: tagSelector,
|
||||||
text: (v) => `:scope :is(*[contains-text="${cssEscape(v)}"])`, // unused; text uses XPath
|
|
||||||
"aria-label": (v) => `[aria-label="${cssEscape(v)}"]`,
|
"aria-label": (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
ariaLabel: (v) => `[aria-label="${cssEscape(v)}"]`,
|
ariaLabel: (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
accessibilityLabel: (v) => `[aria-label="${cssEscape(v)}"]`,
|
accessibilityLabel: (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
@@ -63,8 +62,22 @@ const KNOWN_KEY_TO_CSS: Record<string, (value: string) => string> = {
|
|||||||
hintText: (v) => `[placeholder="${cssEscape(v)}"]`,
|
hintText: (v) => `[placeholder="${cssEscape(v)}"]`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// cssEscape delegates to the platform CSS.escape (per CSSOM spec). It produces
|
||||||
|
// output safe for both identifier and string contexts, since CSS string
|
||||||
|
// literals accept the same `\HEX ` and `\X` escape sequences as identifiers.
|
||||||
function cssEscape(value: string): string {
|
function cssEscape(value: string): string {
|
||||||
return value.replace(/(["\\])/g, "\\$1");
|
return CSS.escape(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAG_NAME = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
||||||
|
|
||||||
|
// tagSelector accepts only valid HTML tag-name characters. Anything else (a
|
||||||
|
// pseudo-class like `*:hover`, a comma, whitespace) would inject CSS into the
|
||||||
|
// surrounding selector. Returning a never-matching selector rather than
|
||||||
|
// throwing keeps the spec running while making the typo visible in logs.
|
||||||
|
function tagSelector(value: string): string {
|
||||||
|
if (!TAG_NAME.test(value)) return ":not(*)";
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectorFromObject(selector: Record<string, string | boolean | undefined>): {
|
function selectorFromObject(selector: Record<string, string | boolean | undefined>): {
|
||||||
|
|||||||
Reference in new issue
Block a user