Repository navigation
Conversation
Spec and implementation plan for #1231: a [rewrite] include_domains allowlist for creative asset rewriting, case-insensitive exclude_domains matching, and a single rewrite of link imagesrcset on the inline path. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Replace to_abs with normalize_creative_url and move exclusion policy to Rewrite::should_proxy_asset and Rewrite::should_wrap_click, sharing the case-insensitive proxy host matcher. Normalize exclude_domains at load. This is the shared step for #1231 and #1234. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…when unset Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
The rewrite pass registers asset handlers only when rewrite_creatives is on and the anchor handler only when click rewriting resolves on. Base removal and TSJS injection run whenever either is on. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…rty proxy Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…avior Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Add the [rewrite] include_domains field with load-time normalization, strict entry validation and blob round-trip tests. Enforcement arrives in the next commit, which applies the list to asset rewriting and first-party sign. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Apply the include list to every asset handler and to first-party sign. Click-through links and fetch-time proxy checks are unchanged. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Describe the review fixes: strict include_domains validation with a separate non-ASCII error, binary-first upgrade and rollback docs, the distinct link imagesrcset candidate, the host-in-both-lists test across every handler, and the shared-step CHANGELOG entry with the sign status change. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Round-trip include_domains entries through url::Host::parse so IPv4-mapped IPv6 and invalid xn-- entries are rejected, warn on exclude_domains entries that never match, and fix the grep, red-step, citation and wording issues raised in review. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
…-creative-asset-host-allowlist
Finish include_domains validation by parsing each entry with url::Host and requiring its rendered form, so dotted IPv4-mapped IPv6 and invalid xn-- entries are rejected. Warn at load on exclude_domains entries that can never match a host. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Collapse the link[href] condition into a let-chain so clippy::collapsible_if does not depend on comment placement, and move the include_domains module-doc bullet after the scheme bullet. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Describe the IPv6 form URL hosts render, the rejection of entries a URL parser would rewrite, and the load-time warning for exclude_domains entries that never match. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
rewrite_creative_html and rewrite_inline_creative_html rewrite assets and clicks unconditionally. Document that production code must use the auction processing entry points, which honor both switches. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
The rewrite pass removed every <base>, which also dropped target="_blank" so raw links opened inside the creative iframe. Drop only the href, which is what can rebase first-party URLs, and remove a <base> left without a target as before, so default output is unchanged. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Name rewrite_clicks in the orchestration example, environment overrides and rollback note, fix the rewrite_creatives = false summary, correct the renderGuard caveat for the sandboxed renderer, and document base target handling and the inert click guard. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
List the CSS re-quoting, https: prefixing and sign error text changes in the Fixed entry, and note that a bidder base target is now kept. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
|
@prk-Jr Your review comments on the spec PRs (#1248 and #1249) are addressed in the spec branches. Those changes are merged here, along with the matching code changes: the |
Brings #1253 at e210f62, including the design spec and plan from its spec/1231-creative-asset-host-allowlist base. #1253 carries its own copy of #1252's rewrite_clicks commits plus the include_domains allowlist, so its conflicts with #1252 are two versions of the same change. Integration decisions: - proxy.rs, creative.rs and settings.rs take a clean three-way merge of #1253 against the RC before #1252. #1253's versions of the shared rewrite_clicks code supersede #1252's, and they also carry #1252's suffix-spoofing test case and doc-link fix. Every other file #1253 touches matches that re-merge exactly, so nothing is duplicated. - Settings normalization keeps main's fallible image_optimizer normalize()? beside rewrite.normalize(). - CHANGELOG.md keeps the RC's entries and takes #1253's exclude_domains, include_domains, rewrite_clicks and rewrite_creatives entries.
aram356
left a comment
There was a problem hiding this comment.
Summary
This PR adds [rewrite] include_domains and applies it to every asset handler on all three HTML paths, to CSS bodies, and to /first-party/sign. It also fixes the inline <link imagesrcset> double proxy. The core change holds up:
- A mutation pass over the new code (12 mutants, run on the PR merged with
main) caught 10. - The merged tree passes
clippy-fastlyandtest-fastly. - Every proxy URL builder goes through
asset_target, so nothing bypasses the allowlist.
Three things block merging:
- the conflict with
main; - the #1252 commits this branch carries;
- an
exclude_domainswarning that tells operators to delete entries that work.
4 of the inline comments below carry a one-click GitHub
suggestion. Use Commit suggestion, or Add suggestion to batch to apply several at once, to add them as commits on the PR branch. The other comments describe the fix in prose, because it spans several hunks or files.
Blocking
🔧 wrench
- The
exclude_domainswarning flags entries that do match, including the trailing-dot workaround. See inline atcrates/trusted-server-core/src/settings.rs:708.
Non-blocking
🤔 thinking
- Sign declines log at
errorand count as 5xx on every adapter. See inline atcrates/trusted-server-core/src/proxy.rs:1692. - The include grammar rejects real hosts (
_), and the0xclause is redundant. See inline atcrates/trusted-server-core/src/settings.rs:3494.
♻️ refactor
- The validation error doesn't name the bad entry. See inline at
crates/trusted-server-core/src/settings.rs:3450.
⛏ nitpick
- Unset
rewrite_clicksfollowsrewrite_creativeson the auction paths. See inline atdocs/guide/creative-processing.md:762. - The spec status is stale. See inline at
docs/superpowers/specs/2026-10-07-1231-creative-asset-host-allowlist-design.md:7.
Cross-cutting / body-level findings
-
🔧 Conflicts with
main. Mergingorigin/main(d2f0d8c) conflicts in two files:CHANGELOG.md: both sides addFixedentries. Keep both.settings.rsnormalize_deserialized: #1221 madeimage_optimizer.normalize()returnResult. The resolution is:
self.rewrite.normalize(); self.image_optimizer.normalize()?;
With that resolution, the merged tree passes
cargo fmt --check,cargo clippy-fastlyandcargo test-fastly(2964 core tests). -
🔧 This branch carries #1252, including #1252's open blocking threads. The base is now
main, so this diff includes therewrite_clickswork in the same state as #1252's7c5eca0da. Review 5507201594 on #1252 still has six open 🔧 threads:auction-orchestration.md;- two CHANGELOG items;
- the example TOML;
- the
creative-processing.mdguard sentence; - the #1234 spec status.
Merging this PR first would land all of them. As the description plans, land #1252 first, then rebase so only the #1231 commits remain. Two related problems:
- The description is out of date. It names
spec/1231-…as the base and lists four commits under review. The base ismain, and the #1231 work now also includesf7885c199,e1332c9fe,2af735d1dand the spec feedback incce4e2a5e. - The shared
Fixedentry in CHANGELOG.md no longer matches #1252's copy. This one adds "and other entries that can never match a host are logged with a warning". The plan says the shared text is identical in both PRs, so whichever lands second will conflict on that line.
-
📌 The trailing-dot bypass needs its follow-up issue. The spec's Follow-ups section says to track it in a follow-up issue, and I found none.
https://blocked.example.com./xhashost_str()blocked.example.com., so it escapesexclude_domainsand still getsts-ec. The spec suggests stripping one trailing.before matching. Until then, an exclude entry written with the trailing dot is the stopgap, which is why the wrench above matters.
CI Status
- CLAUDE.md symlink guard: PASS
- browser integration tests: PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo fmt: PASS (required)
- cargo test: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- prepare integration artifacts: PASS
- vitest: PASS
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Please address the host-classification issue: valid DNS names such as 0xproxy are rejected as IPv4 candidates, preventing otherwise valid allowlist configurations.
Also clarify the changelog and operator guidance: include_domains controls newly rewritten or signed asset targets, not fetch-time enforcement. Previously issued proxy URLs can still forward EC IDs to hosts subsequently removed from the list. This is a documentation correction, not a request to expand runtime policy.
Give exclude_domains its own looser never-matches check so trailing-dot, underscore and wildcard-IP entries are not flagged, name the bad entry and reason in include_domains validation errors, accept underscores and 0x-prefixed DNS labels in include_domains, and document that include_domains governs newly rewritten or signed URLs rather than fetch-time enforcement. Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me> # Conflicts: # CHANGELOG.md # crates/trusted-server-core/src/settings.rs
… sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Approved. No findings to raise on the creative asset host allowlist and imagesrcset double-proxy fix.
aram356
left a comment
There was a problem hiding this comment.
Summary
Round 2 at e15eea5a1. The round-1 fixes in 2feb489 work. I checked each one with the PR-head ts binary and a mutation pass:
exclude_domainshas its own looser check, soblocked.example.com.,ads_cdn.example.comand*.example.netare no longer flagged.include_domainsacceptscdn_assets.example.comand0xproxy, and still rejects0xc0000201.- A bad
include_domainsentry is now named, along with the reason.
The mutation pass made 8 deliberate breaks to the new code, and the tests caught 7. The one they miss is deleting the warning loop, which is log-only. The sign-decline status and log level are now tracked in #1394, and the trailing-dot bypass in #1393.
The PR merged with current main passes cargo fmt --check, clippy-fastly and test-fastly (2978 core tests).
Two things still block merging:
- the CodeQL autofix stripped the
exclude_domainswarning back down; - this PR's copy of #1252 is now out of date.
1 of the inline comments below carries a one-click GitHub
suggestion. Use Commit suggestion to apply it.
Blocking
🔧 wrench
- The CodeQL autofix removed the entry and the reason from the
exclude_domainswarning. See inline atcrates/trusted-server-core/src/settings.rs:712.
Cross-cutting / body-level findings
-
🔧 This PR's copy of #1252 is out of date, and either merge order now conflicts heavily.
- #1252 picked up 89d4897 ("Align creative click switch docs and tests with review feedback"), which this branch doesn't have.
- The description's claim that the six commits below the #1231 work are "cherry-picks of #1252 with identical patch IDs, and are reviewed there" no longer holds.
- Measured conflicts with
git merge-tree: merging #1252's head into this branch, or this branch after #1252 lands, conflicts in 11 files with 29 hunks, about 840 conflicting lines in total. Most of it is increative.rs,settings.rsandproxy.rs. - If this PR landed first,
mainwould get #1252's code without 89d4897.
Rebuild instead of merging. Once #1252 lands, rebuild this branch on
mainby cherry-picking only the #1231 commits:53cfb8149 48d59b8da ef8ad67f9 9d9fb2733 ea642da7e 2dfe7e7b5 cce4e2a5e f7885c199 e1332c9fe 2af735d1d 2feb48970 8aabb3920 e15eea5a1I ran that onto
main+ #1252 atefdd8a00e:- Five commits conflict, with 8 small hunks between them:
48d59b8da: theexclude_domainsdoc comment insettings.rs;ef8ad67f9:creative.rsandsettings.rs;2dfe7e7b5: CHANGELOG andcreative-processing.md;2af735d1d: CHANGELOG;2feb48970: CHANGELOG and the sign# Errorsdoc.
- The result touches only the 12 #1231 files.
- It passes
cargo fmt --check,clippy-fastlyandtest-fastly(2978 core tests).
I resolved the conflicts by taking the #1231 side, so read the CHANGELOG and doc hunks rather than trusting that resolution. In particular, the shared
Fixedentry should keep #1252's wording plus the one #1231 clause about the new warning.
CI Status
- Analyze (actions): PASS
- Analyze (javascript-typescript): PASS (two runs)
- Analyze (python): PASS
- Analyze (rust): PASS
- browser integration tests: PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo fmt: PASS (required)
- cargo test: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native) (macos-latest): PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- CLAUDE.md symlink guard: PASS
- CodeQL: PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- prepare integration artifacts: PASS
- vitest: PASS
| if Self::check_exclude_domain(pattern).is_err() { | ||
| log::warn!("rewrite.exclude_domains: found an entry that never matches a URL host"); | ||
| } |
There was a problem hiding this comment.
🔧 wrench: The CodeQL autofix in 8aabb39 removed both the entry and the reason from this warning, which undoes the round-1 fix. With the PR-head ts binary, ts config validate --no-env on exclude_domains = ["https://cdn.example.com", "cdn.example.com:443", "blocked.example.com.", "ads_cdn.example.com", "*.example.net"] exits 0 and prints:
rewrite.exclude_domains: found an entry that never matches a URL host
rewrite.exclude_domains: found an entry that never matches a URL host
Two different entries produce two identical lines, and neither line names an entry. configuration.md tells the operator to "fix them, or their hosts stay proxied", but the warning gives them nothing to find the entries with short of bisecting the list.
With the suggestion applied, the same config prints:
rewrite.exclude_domains: entry `https://cdn.example.com` never matches a URL host (not a valid URL host)
rewrite.exclude_domains: entry `cdn.example.com:443` never matches a URL host (not a valid URL host)
Alert 210 is the same false positive this repo has dismissed many times. CodeQL tracks taint through Settings field-insensitively, so every field looks as though it carries the resolved secrets. Alert 171 is the closest match: an asset-route prefix logged from Settings, dismissed as "False positive from field-insensitive taint through Settings after app-config secret resolution… The prefix is not a credential". An exclude_domains entry is a host name that the operator writes in the TOML and that also appears in served creatives.
The suggestion restores the round-1 line, which makes settings.rs byte-identical to 2feb489. CodeQL will probably reopen alert 210; dismiss it as a false positive with the same reasoning as 171.
| if Self::check_exclude_domain(pattern).is_err() { | |
| log::warn!("rewrite.exclude_domains: found an entry that never matches a URL host"); | |
| } | |
| if let Err(reason) = Self::check_exclude_domain(pattern) { | |
| log::warn!( | |
| "rewrite.exclude_domains: entry `{pattern}` never matches a URL host ({reason})" | |
| ); | |
| } |
Summary
[rewrite] include_domains, a host allowlist for creative asset rewriting./first-party/proxy, and each proxied fetch appends the EC ID, so every third-party asset host receives it.exclude_domainsstill wins over the list, and click-through links are never affected./first-party/signapplies the same policy for absolute and//input.502, the same as an excluded host, so the creative runtime loads the raw URL directly.403: still means only "blocked byproxy.allowed_domains".ts config push. That includes schemes, ports, paths, stray dots, unbracketed IPv6, and non-ASCII names, which must be written in punycode.<link imagesrcset>was proxied twice, wrapping the proxy URL inside another proxy URL.Implements the approved design in #1248:
docs/superpowers/specs/2026-10-07-1231-creative-asset-host-allowlist-design.md.Stacked on #1252. This branch builds on the #1234 implementation, which introduces the shared normalizer and host-policy split.
48d59b8d,ef8ad67f,9d9fb273and2dfe7e7b, plus the review fixes in2feb4897. Merges ofmainadd nothing of their own beyond conflict resolution.d8f04390…ee5715f1) are cherry-picks of Make creative click rewriting a separate switch #1252 with identical patch IDs, and are reviewed there.main. **Spec**: creative asset host allowlist #1248 was closed without merging, so this PR carries the spec itself.mainand only the Add a host allowlist for creative asset rewriting #1231 commits remain.Changes
These are the #1231 commits only.
crates/trusted-server-core/src/settings.rsinclude_domainsfield (left out of the blob when empty), load-time normalization, strictvalidate_include_domains, and the include clause inshould_proxy_assetviais_host_permitted; testscrates/trusted-server-core/src/creative.rslink[href]no longer rewritesimagesrcset; the[imagesrcset]handler rewrites it once. Tests and module docscrates/trusted-server-core/src/proxy.rsis_host_permittedmadepub(crate); sign decline log; an 8-case GET/POST sign matrix covering200,502and403crates/trusted-server-core/src/config_payload.rsdocs/guide/configuration.md,creative-processing.md,first-party-proxy.md,api-reference.md502. Fixes the*.cdn.example.comapex exampletrusted-server.example.tomlinclude_domainsexampleCHANGELOG.mdinclude_domains; Fixed the inlineimagesrcsetdouble proxyCloses
Closes #1231
Test plan
cargo test-fastly && cargo test-axum. Alsocargo test-fastly-reuse,cargo test-cloudflare,cargo test-spin,./scripts/test-cli.shand the parity tests.cargo clippy-fastly && cargo clippy-axum. All 8 clippy aliases pass, and every intermediate commit passesclippy-fastly.cargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest run(no JS changes)cd crates/trusted-server-js/lib && npm run format(no JS changes)cd docs && npm run format, plus the markdown prettier check outsidedocs/cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveimagesrcsetregression test was seen failing on the unfixed code. Make creative click rewriting a separate switch #1252's switch-matrix tests pass together with this branch's allowlist tests.Rollout: deploy the binary first, then add
include_domainsand runts config push. Rollback: removeinclude_domainsand push first, then roll back the binary; older binaries reject the key because ofdeny_unknown_fields.Hardening note:
include_domainsentry fails settings validation with a config error rather than a panic. Covered byrewrite_include_domains_reject_malformed_entries.exclude_domainsentries: empty and bare"*"entries are dropped with a warning instead of rejected, so existing configs keep loading after an upgrade.Checklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)