Harden config colour/size values against style-attribute injection#65
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfa089ffc5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // var(--token) with an optional simple fallback restricted to the characters a | ||
| // colour value uses — no `;` `{` `}` `(` `)` and no injection-adjacent punctuation, | ||
| // e.g. var(--primary, #03a9f4). Tighter than "anything but delimiters". | ||
| const VAR = /^var\(\s*--[a-z0-9-]+\s*(?:,\s*[a-z0-9\s.,%/#-]*)?\)$/i; |
There was a problem hiding this comment.
Preserve valid nested CSS variable colors
When a user configures a color with a nested theme fallback such as var(--ha-card-background, var(--card-background-color, #fff)), this allowlist rejects it because the fallback part of var() cannot contain parentheses. Nested var() fallbacks are valid CSS and common in Home Assistant theme values, so existing configured backgrounds, text colors, active colors, or ripple colors silently revert to the hard-coded default after this change.
Useful? React with 👍 / 👎.
Addresses the Codex review on nicosandller#65 (nested var() fallbacks) and a wider gap it pointed at: Home Assistant stores theme colours as bare "r, g, b" triplets read back as rgb(var(--rgb-primary-color)), and chains fallbacks as var(--a, var(--b, #fff)). The first-pass allowlist rejected both, so real configs would have silently reverted to defaults. Replace the fixed colour-form regexes with a structural, fail-closed check: allowed characters only (no ; { } " ' : @ \ ! or control chars), balanced parens, and every function call on a SAFE_FUNCS allowlist (colour/gradient/var/ env/maths). Arbitrary nesting like rgb(var(--x)) and var(--a, var(--b, #fff)) now passes, while url()/image-set()/expression()/attr()/element()/paint() can never appear. Verified against real HA theme values and an adversarial + fuzz battery; O(n), no ReDoS or recursion (200k-char and 5000-deep inputs resolve in <=1ms). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catch from the Codex pass — the follow-up commit addresses it, and it turned out to point at something broader. The nested Rather than special-case those, I swapped the fixed colour-regexes for a structural check: allowed characters only (no I checked it against theme-colour forms from across HA versions — named, hex, One trade-off worth flagging: it's slightly more permissive than a strict grammar — an invalid value like |
|
Following up on my own PR with a gap I found while re-auditing this. The colour/size sanitiser is solid, but Fix is ready and verified: coerce both sinks through |
Config is shareable/importable, so its colour and size fields are attacker-controlled. Lit does not escape `;`/`}` inside a style-attribute expression, so e.g. `background: "red;position:fixed;inset:0;z-index:99999"` paints a full-viewport overlay over HA, and a colour containing `url(...)` becomes an IP-leaking remote fetch. (Addresses nicosandller#64.) Adds src/css-safe.ts: an allowlist `cssColor`/`cssColorOr` (hex, named/CSS-wide keywords, rgb/hsl/oklch/... functions, and `var(--x[, fallback])`) and a `cssNumber` coercion for size/angle fields. Wires it at every `style="…"` sink in the card, editor preview, and shared render helpers (stage background, text colour/size, item badge size/angle, opening accent, ripple colour/size). SVG `fill=`/`stroke=`/`transform=` presentation attributes are intentionally left alone — there is no declaration there to break out of. Allowlist verified against the colours real configs use (HA theme vars, hex, named, rgb/rgba, hsl, oklch/lab/lch/hwb/color) so no legitimate value regresses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the Codex review on nicosandller#65 (nested var() fallbacks) and a wider gap it pointed at: Home Assistant stores theme colours as bare "r, g, b" triplets read back as rgb(var(--rgb-primary-color)), and chains fallbacks as var(--a, var(--b, #fff)). The first-pass allowlist rejected both, so real configs would have silently reverted to defaults. Replace the fixed colour-form regexes with a structural, fail-closed check: allowed characters only (no ; { } " ' : @ \ ! or control chars), balanced parens, and every function call on a SAFE_FUNCS allowlist (colour/gradient/var/ env/maths). Arbitrary nesting like rgb(var(--x)) and var(--a, var(--b, #fff)) now passes, while url()/image-set()/expression()/attr()/element()/paint() can never appear. Verified against real HA theme values and an adversarial + fuzz battery; O(n), no ReDoS or recursion (200k-char and 5000-deep inputs resolve in <=1ms). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The colour/size sanitiser missed the stage's aspect-ratio: width/height reach
`aspect-ratio: ${c.width} / ${c.height}` raw. The card's setConfig guards them
as numbers, but the editor's setConfig does not, so a config imported and opened
in the editor breaks out of the style attribute (verified in the dev harness:
a crafted width makes .stage compute position:fixed).
Coerce both sinks through cssNumber(c.width, DEFAULT_WIDTH) / (c.height,
DEFAULT_HEIGHT). Also treat a blank string as unset in cssNumber — Number("")
is 0 (finite), which would silently become a 0 ratio. Adds regression tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0e9ae39 to
c75d708
Compare
Label span resolution per the review: keep #41's inflow fallback AND this branch's labelSize — the span now carries both. itemBadgeLabel gains main's #39 guard (no entity, no state line; an unbound device's label can only be its configured name). Review point 2: labelSize is clamped/coerced at the style sink (itemLabelSize, 8–40, default 12) in both card and editor, so a config string like "20px;color:red" can never reach the style attribute. Unit-tested with the review's exact payloads; interim hardening until #65's cssNumber lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ler#62) and route labelSize through cssNumber One conflict (badge size line vs nicosandller#58's animation resolver) — union: the sanitized size and the animation both stay. Post-merge, nicosandller#62's interim labelSize clamp now delegates its coercion to cssNumber, so every numeric style sink shares one guard; the 8-40 range clamp stays on top. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nicosandller
left a comment
There was a problem hiding this comment.
Approved — and we agree with the scope as drawn: style-attribute sinks only, SVG presentation attributes deliberately excluded (no declaration to break out of there), numeric fields coerced rather than pattern-matched, allowlist over denylist. That's the right boundary for this bug class, and the test suite — legit HA colours across every theme era, the hand-written attack corpus, and the 500-case fuzz invariant — is the most thorough in the repo.
I've rebased the branch on main (pushed to your fork — hope that's welcome; it was the same mechanical churn as the other rebases this week):
- One conflict, the badge
sizeline vs #58's animation resolver — resolved as a union, so the sanitized size and the animation class both stay. - #62's
labelSizenow routes throughcssNumberexactly as your PR body anticipated:itemLabelSizekeeps its 8–40 range clamp but delegates coercion to your shared guard, so every numeric style sink uses one primitive. - Audited the merged tree for style sinks that arrived after your branch (#57/#58/#60/#62) — all covered; no unsanitized interpolation remains.
Verified end-to-end: tsc clean, 341/341 tests, build clean, and a live harness pass — the demo attack (red;position:fixed;inset:0;z-index:99999 as background) falls back to the theme default, a url(…)-smuggling text colour is neutralized, hostile size/labelSize strings coerce to defaults, while var(--primary-color, #03a9f4) and the rgba(var(--rgb-primary-color), 0.5) triplet idiom render byte-identical and the merged features (icon animation, labels, rotation) still work.
Two style sinks in my still-open branches (#63's roll curtain scaleY, #69's editor preview) will adopt these helpers when they rebase after this lands. Ready to merge.
Config is shareable/importable, so its colour and size fields are effectively attacker-controlled. Lit doesn't escape
;or}inside a style-attribute expression, so a shared/imported config can break a value out of its declaration — e.g.background: "red;position:fixed;inset:0;z-index:99999"paints a full-viewport overlay over the HA UI, and a colour containingurl(…)becomes an IP-leaking remote fetch.What this does
src/css-safe.ts: an allowlistcssColor/cssColorOr(hex, named + CSS-wide keywords,rgb/hsl/hwb/lab/lch/oklab/oklch/color(...), andvar(--x[, fallback])) plus acssNumbercoercion for numeric fields. Allowlist over denylist on purpose — a denylist invites the one vector you forgot.style="…"sink in the card, the editor preview, and the shared render helpers: stagebackground, textcolor/size, item badgesize/angle, openingaccent(thefill/strokeinstyle=), and ripplecolor/size.fill=/stroke=/transform=presentation attributes alone — there's no CSS declaration there to break out of, so they're not this bug class.Honest scoping
size/anglenumeric coercion is a simple finite-number guard (new, not previously battle-tested, but small and covered by tests).width/height/gridalready go through yoursetConfignumeric check, so those are left as-is.labelSizefield of the same shape; it's not onmainso it's out of scope here — it just needs the samecssNumberwrap wherever it lands.Backward-compatible: every legitimate value renders unchanged — I tested the allowlist against the colours real configs use (HA theme vars with/without fallback, hex, named, rgb/rgba in comma and modern space syntax, hsl/hsla, oklch/lab/lch/hwb/color). Only invalid/malicious values fall back to the existing defaults.
tscclean; full suite green (adds sanitiser unit + adversarial/fuzz cases).Marked as a draft — happy to adjust scope, naming, or split it if you'd prefer smaller.