Repository navigation
fix(ssr): validate dynamic attribute and tag names - #592
Merged
Merged
Conversation
ssrElement and ssrSpread wrote spread keys and the tag name as given, so a runtime name containing whitespace, a quote, `<`, `>`, `/`, `=` or a control character was read back by the HTML parser as more than one name. Such spread keys (including `attr:` and `bool:` names) are now dropped, as the client's setAttribute would reject them, and ssrElement throws for such a tag name, as document.createElement does. Valid names render exactly as before; validated names are kept in a bounded cache, so a repeated key costs one lookup. ssrSpread now escapes class/className values the way ssrElement does, and ssrClassList, ssrStyle and ssrStyleProperty attribute-escape class names and style property names. Co-authored-by: Cursor <cursoragent@cursor.com>
|
ryansolid
added a commit
to solidjs/solid
that referenced
this pull request
Oct 7, 2026
SSR validates dynamic attribute and tag names, and escapes class and style names (ryansolid/dom-expressions#592). The lockfile is refreshed for 0.40.12, which also updates the stale babel-preset-solid solid-js peer entry left by the 1.9.16 release. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ssrElementandssrSpreadwrote spread keys and the tag name exactly as given. A runtime name containing whitespace, a quote,<,>,/,=or a control character is read back by the HTML tokenizer as more than one name (or ends the tag). This PR makes SSR validate those runtime names, using the same grammar as Solid 2.0:/[\0-\x20\x7F-\x9F"'<>/=]/.Legitimate names are unaffected:
@click,x-on:click,:class,xlink:href,data-*,aria-*, custom elements (my-widget,x-foo.bar,math-α),foreignObject,svg:rect.Changes (
packages/dom-expressions/src/server.js)ssrElement/ssrSpread(plain,attr:andbool:branches) that are not a single attribute name are dropped, as the client'ssetAttributewould reject them. A dropped key spaces the output exactly like an existing skipped key (e.g. anundefinedvalue).ssrElementthrow"<tag>" is not a valid tag name, matching clientdocument.createElementand Solid 2.0. In solid-js this is reachable through<Dynamic component="...">with a string.ssrSpreadnow escapesclass/classNamevalues withescape(..., true), the same asssrElementalready did.ssrSpreadis no longer emitted by the compiler but is still exported for older compiled output.ssrClassList,ssrStyle,ssrStylePropertyattribute-escape class names and style property names, so a"in a name cannot end the quotedclass="…"/style="…"attribute.ssrClassList(obj)is written directly insideclass="…"by the compiler, and computed style keys (style={{ [k]: v }}) reachssrStylePropertyask + ":". Escaping in the runtime covers already-compiled code without a compiler change.escapeitself is unchanged.Performance
Validated names are kept in bounded module-level caches (512 entries; only valid names are cached). For spread keys the cache stores the final written name (
Aliases[prop] || escape(prop)), so a hit replaces the validation, the alias lookup and theescapecall. The output is identical because it is the same calculation, just cached. Style names use a similar cache aroundescape(name, true).Micro-benchmark, 100k renders, median ms (Node 26):
ssrElement, 10 spread keysssrElement, 10 keys + 2 alternating tagsssrSpread, 10 keysssrStyle, 4 keysssrClassList, 4 keysssrStyleProperty, 4 callsOutput changes
Output for every valid name is byte-identical to before (including the existing
data-a&b→data-a&bbehavior). The byte-level changes are:attr:,bool:) that are not a single attribute name are dropped inssrElementandssrSpread.ssrElementthrows"<tag>" is not a valid tag namefor an invalid tag name (reachable via solid-js<Dynamic component="...">).ssrSpreadattribute-escapesclass/classNamevalues, so&and"in them change bytes (now matchingssrElement).ssrClassListkeys containing"now render as". Keys containing<now render raw instead of as<; that is a byte change, but the parsed value is the same.ssrStyleandssrStylePropertyescape style names containing&or".classis combined with a dynamicclassList(e.g.<div class="a" classList={obj}>), the compiler wraps thessrClassListresult in anotherescape(..., true). A"in a class name there is now double-escaped and shows as a literal". It stays inside the attribute, but the value is wrong;&already behaved this way on that path before this PR. The proper fix is for the compiler to skip the outer escape forssrClassListcalls.Tests
dynamic attribute and tag namesblock inpackages/dom-expressions/test/ssr/ssr.spec.js(14 tests): invalid keys in all three branches for both helpers, spacing when a key is dropped, invalid tags throw, custom element / SVG tags accepted, byte-identical output for valid names, behavior on repeated renders and past the cache bound,ssrSpreadclass escaping, and name escaping inssrClassList/ssrStyle/ssrStyleProperty. 10 of them failed before the change.main, 1.9.16) against this branch: its suites pass (487 client, 8 server), and the builtsolid-js/webserver bundle drops invalid keys, escapes thessrSpreadclass value, and throws for an invalid<Dynamic>tag. Valid<Dynamic>output is byte-identical to the 0.40.11 build.Prepared with Claude via Cursor