Skip to content

fix(composition): reject raw-text terminators and escapes in style values - #440

Open
Wenjix wants to merge 1 commit into
hypit-ai:mainfrom
Wenjix:security/visual-style-hardening
Open

Wenjix wants to merge 1 commit into
hypit-ai:mainfrom
Wenjix:security/visual-style-hardening

Conversation

@Wenjix

@Wenjix Wenjix commented Oct 27, 2026 •

Copy link
Copy Markdown

Closes #448

Summary

Two classes of escape reaching the HTML <style> block and CSS resource functions through assertVisualStyleV1 in packages/composition/src/visual.ts:

  • 40 (medium, CWE-79): animation keyframe values are emitted verbatim into a raw-text <style> element by packages/html-program/src/document.ts. The validator blocked ;, {} and control characters but not <, so a value like red</style><script>alert(1)</script> terminates the element and injects markup into the render document.
  • 58 (low, CWE-918) + 38 (low): the url(/image-set(/var(/env(/attr( checks are literal substring/regex checks, but CSS resolves backslash escapes before recognizing functions — u\72 l(https://evil/), linear-gradient(red\2c url(evil)) pass the literal checks and decode to resource-bearing or syntax-altering CSS in the browser, smuggling a fetch past the typed-Artifact policy.
  • Fix: style values now reject both < and \ outright. Neither character is needed by any property in the supported style set, and assertColor (the sibling validator for paint values) already rejected both — this brings style values to parity.

Behavior relative to main

Authored value main this PR
transform: translateX(4px) accepted accepted
color: red</style><script>… accepted → breaks out of <style> rejected at validation
background: u\72 l(https://evil/) accepted → browser fetches rejected at validation
animation keyframe holding </style> accepted → injected markup rejected at validation

Red / green

packages/composition/test/track.test.ts: three new cases — a </style><script> value, a u\72 l() escaped-URL value, and an animation keyframe carrying the terminator — each fails on main (value admitted) and throws here.

Verification

  • node --import tsx --test packages/composition/test/track.test.ts — 13 pass.
  • pnpm check — clean.

Merge Danger

  • Style values containing a literal < or \ are now rejected. Both were already unsafe; no legitimate CSS in the supported set needs them.

Checklist

  • The why is documented in this description.
  • Regression tests added; fail on main, pass here.
  • TypeScript check clean.

Generated with Devin

…values

Style declarations are serialized into a raw-text <style> element and
inline style attributes. Two gaps let authored values break the
typed-Artifact resource policy:

- `<` terminates the <style> element at compile time, letting a keyframe
  or element style introduce executable markup into the render document.
- A backslash opens a CSS escape, so `u\72 l(...)` reaches the browser
  as `url(...)` while evading the literal `url(`/`image-set(` checks.

Reject both characters as unsafe style values. Legitimate V1 values
(transforms, colors, gradients, filters) never require them.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 27, 2026 06:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(composition): style values admit raw-text breakouts and CSS escape bypasses

2 participants