Match CSS url() quoting rules when rewriting creative styles - #1106
Draft
prk-Jr wants to merge 7 commits into
Draft
Match CSS url() quoting rules when rewriting creative styles#1106prk-Jr wants to merge 7 commits into
prk-Jr wants to merge 7 commits into
Conversation
Treat a url() value as quoted only when a matching closing quote is present, and end a quoted value at that quote rather than at the first paren, so a value is rewritten the way a browser reads it. Derive srcset candidate state as the scan advances rather than from the candidate prefix at each comma.
prk-Jr
marked this pull request as draft
September 1, 2026 15:30
The rewriter does not resolve CSS escapes, so a value carrying a backslash cannot be mapped to the resource the page will actually request; proxying the raw bytes points somewhere else. A raw newline, which preprocessing also produces from a carriage return or a form feed, makes the value a bad string the browser discards, so rewriting it proxies a URL that is never fetched. Both are now passed through untouched. Also fold an escaped CRLF into a single escaped newline when locating the end of a quoted string, and end the string at a form feed, so the extent matches what preprocessing produces.
Inserting the resolvability helper above css_string_end left that function's doc comment attached to the new helper.
Collaborator
Author
|
Closes #1114 |
The scan bounded a value by the next quote and paren, so a span could fuse across declarations, a missing paren abandoned the rest of the input, an escape became a URL nobody requests, and a value ending at input was skipped. It also stepped back from a computed index, which slices a multi-byte character and aborts the guest. Reading with a tokenizer supplies both the extent and the resolved value, and leaves a malformed value — which a browser discards anyway — on its original bytes. Cover the other references a browser fetches: src(), a bare string candidate in image-set(), and an @import prelude, which takes one URL and reads later strings as media queries. A string counts as a URL only in those places, so font-family and content keep theirs, and an @import is a rule only where a top-level rule may start — the same token is data inside a declaration, in another prelude, and in a style attribute, which is not a stylesheet. The walk recurses per scope through upstream-supplied CSS, so it is bounded, and CSS past the bound is rejected rather than passed through below it. cssparser already built here as a transitive dependency.
… fix/creative-parser-bounds
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
url()rewriter parsed quoting by position rather than by matching delimiters, so it disagreed with how a browser reads the same declaration. Values were rewritten in ways the browser would not, and some were left unrewritten.). The rewriter ended the value at the first)afterurl(, which truncated such values and left the intended URL unproxied.split_srcset_candidatesre-derived per-candidate facts from the whole candidate prefix at each comma. Those facts belong to the candidate, so they are now tracked as the scan advances.Changes
crates/trusted-server-core/src/creative.rsrewrite_style_urls: treat a value as quoted only when a matching closing quote is present; locate the closing paren after the closing quote so a quoted value keeps an inner)crates/trusted-server-core/src/creative.rscss_string_endhelper: finds the quote that closes a CSS string, honoring backslash escapes and treating a raw newline as ending the stringcrates/trusted-server-core/src/creative.rssplit_srcset_candidates: derive candidate scheme and whitespace state as the scan advances instead of from the candidate prefix at each comma; corrected the doc note, which described behavior the function did not havecrates/trusted-server-core/src/creative.rsdata:casesBehavior
url("https://cdn.example/a)b.png")url("https://cdn.example/plain.png")url('/local/a.png")url("https://cdn.example/a+ newline +b)srcset="data:...;base64,,,,, 1x, /b.png 2x"Unquoted values still end at the first
), matching the CSS url-token rule.Verification against production CSS
Captured the 18 unique stylesheets served through a local
fastly compute servesession and ran both themainand branch versions ofrewrite_css_bodyover the identical corpus. Output is byte-identical on all 18, and both leave every file unmodified — the values there aredata:URIs, whichto_absdeclines. No change to real traffic.Closes
Closes #1114
Test plan
cargo test-fastly && cargo test-axum(alsotest-cloudflare,test-spin)cargo clippy-fastly && cargo clippy-axum(alsoclippy-cloudflare,clippy-cloudflare-wasm,clippy-spin-native,clippy-spin-wasm,trusted-server-cli)cargo fmt --all -- --checkcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1Checklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!) — no logging added