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
Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Reviewed the complete diff against main, including the stacked changes: creative HTML/CSS/srcset rewriting, host normalization and validation, signing and proxy policies, click controls, auction and SSAT delivery, browser runtime interactions, config serialization, CLI overlays, and rollout compatibility. No blocking correctness or security findings.
Validation
- All 2,984 core unit tests passed when running the compiled test binary directly with TRUSTED_SERVER__GIT_VERSION absent. Cargo's test launcher exposes an existing environment collision affecting 12 legacy config tests; the responsible build-script behavior is unchanged from main.
- Rust formatting, changed documentation formatting, and diff whitespace checks passed.
- Full adapter verification was not rerun locally. Remote checks still pending below remain subject to completion.
CI Status
- CodeQL: SKIPPED
- cargo test (ts CLI, native) (macos-latest): PASS
- format-typescript: PASS
- cargo test (cross-adapter parity): PASS
- cargo test: PENDING
- Analyze (python): PASS
- Analyze (javascript-typescript): PASS
- vitest: PASS
- cargo test (axum native): PENDING
- cargo fmt: PASS
- Analyze (javascript-typescript): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- format-docs: PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- docs-proposal scripts: PASS
- cargo test (ts CLI, native) (ubuntu-latest): PASS
- Analyze (actions): PASS
- prepare integration artifacts: PENDING
- Analyze (rust): PENDING
- CLAUDE.md symlink guard: PASS
aram356
left a comment
There was a problem hiding this comment.
Please address the malformed exclude-domain warning in this owning PR before it is refreshed into RC 202610. The RC's CodeQL check reports this new logging sink as alert #210.
This request concerns the warning added here. The other RC alerts involve code already on main or test fixtures and need separate triage; changing URL encryption algorithms is not warranted by those findings.
…set-host-allowlist Signed-off-by: dhruv8sh <dhruv8sh@proton.me> # Conflicts: # CHANGELOG.md
Name each inert entry by its configured index and a static reason instead of echoing the raw value, which may be a pasted URL carrying credentials or control characters (CodeQL alert #210). Signed-off-by: dhruv8sh <dhruv8sh@proton.me>
aram356
left a comment
There was a problem hiding this comment.
Summary
Round 4 at 83fe9f5d5. Since round 3, the author fixed the P2 from review 5606398526 and merged main:
- The P2 fix works. The warning now gives the entry's position, counted before empty and
*entries are dropped, plus a fixed reason, and a test with a credential sentinel covers it. With the PR-headtsbinary, anexclude_domainsentry carryingexample-user:example-secretprintsrewrite.exclude_domains[2] never matches a URL host (not a valid URL host); the entry is kept, with no credential. - CodeQL is clear. Alert 210 is marked fixed on this PR's merge ref.
- The
mainmerge is correct.d342e47dfkept bothCHANGELOG.mdentries. - CI is green. All 23 checks pass.
Two things still block merging:
validate_include_domainsstill echoes the raw entry, which is the same leak on the other list;- this PR's copy of #1252, which is now three commits behind.
1 of the inline comments below carries a one-click GitHub
suggestion. Use Commit suggestion to apply it.
Blocking
🔧 wrench
include_domainsvalidation errors echo the raw entry, credentials included. See inline atcrates/trusted-server-core/src/settings.rs:3617.
Non-blocking
⛏ nitpick
- The
normalizedoc doesn't cover the new warnings, and no test pins their ordering. See inline atcrates/trusted-server-core/src/settings.rs:692.
Cross-cutting / body-level findings
-
🔧 This PR's copy of #1252 is now three commits behind. It lacks 89d4897, 70ba721 and fa87a49. Merging #1252's head into this branch, or this branch after #1252, now conflicts in 11 files with 37 hunks.
Once #1252 lands, rebuild this branch on
mainwith the round-3 recipe, adding the P2 fix at the end:53cfb8149 48d59b8da ef8ad67f9 9d9fb2733 ea642da7e 2dfe7e7b5 cce4e2a5e f7885c199 e1332c9fe 2af735d1d 2feb48970 83fe9f5d5I re-ran it onto
main+ #1252 atfa87a4989. The same nine hunks conflict, and round 3's per-hunk resolutions still apply.83fe9f5d5applies cleanly on top. The result touches only the 12 #1231 files. It passescargo fmt --check, docs prettier,clippy-fastlyandtest-fastly(3009 core tests).The same warning applies: don't resolve with
-X theirsorgit checkout --theirs. The first dropsmain'sCHANGELOG.mdentries and #1252's<base target>doc; the second takes the whole file and reverts unrelated code.
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
- docs-proposal scripts: PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- prepare integration artifacts: PASS
- vitest: PASS
| err.add_param("reason".into(), &reason); | ||
| err.message = Some( | ||
| format!( | ||
| "rewrite.include_domains entry `{pattern}` must be a host name with an optional leading `*.`, a dotted-quad IPv4 address or a bracketed IPv6 address ({reason})" |
There was a problem hiding this comment.
🔧 wrench: The P2 fix keeps raw values out of the exclude_domains warning, but validate_include_domains still puts the raw entry into both of its error messages. I asked for the entry to be named back in round 1, before the P2 made the case for positions instead, so this is the same leak on the other list.
With the PR-head ts binary, include_domains = ["assets.example.com", "https://example-user:example-secret@cdn.example.com/path"] fails ts config validate --no-env with:
[ts] typed app-config failed validation: rewrite.include_domains: rewrite.include_domains entry `https://example-user:example-secret@cdn.example.com/path` must be a host name with an optional leading `*.`, a dotted-quad IPv4 address or a bracketed IPv6 address (character outside a-z, 0-9, `-`, `_` and `.`)
The exposure is narrower than the warning's was. The edge prints only the error code, through validation_error_summary. The message still reaches the operator's terminal and any CI log that runs ts config validate or push. The non-ASCII branch has the same problem, and so does the value param.
An index-based message still lets the operator find the entry. normalize doesn't drop include_domains entries, so the position matches the TOML on both the CLI and the edge:
fn validate_include_domains(patterns: &[String]) -> Result<(), ValidationError> {
for (index, pattern) in patterns.iter().enumerate() {
if !pattern.is_ascii() {
let mut err = ValidationError::new("non_ascii_rewrite_include_domain");
err.add_param("index".into(), &index);
err.message = Some(
format!(
"rewrite.include_domains[{index}] is not ASCII; write internationalized domain names in punycode (xn--) form"
)
.into(),
);
return Err(err);
}
if let Err(reason) = check_include_domain(pattern) {
let mut err = ValidationError::new("invalid_rewrite_include_domain");
err.add_param("index".into(), &index);
err.add_param("reason".into(), &reason);
err.message = Some(
format!(
"rewrite.include_domains[{index}] must be a host name with an optional leading `*.`, a dotted-quad IPv4 address or a bracketed IPv6 address ({reason})"
)
.into(),
);
return Err(err);
}
}
Ok(())
}rewrite_include_domains_report_the_bad_entry_and_reason then needs to look for the position. This version also covers the credential case:
#[test]
fn rewrite_include_domains_report_the_bad_entry_and_reason() {
let err = validate_include_domains(&[
"assets.example.com".to_owned(),
"https://example-user:example-secret@cdn.example.com/path".to_owned(),
])
.expect_err("should reject an entry with a scheme and credentials");
let params = format!("{:?}", err.params);
let message = err.message.expect("should set a message").into_owned();
assert!(
message.contains("rewrite.include_domains[1]") && message.contains("character outside"),
"should name the bad entry by index and give the reason: {message}"
);
assert!(
!message.contains("example-secret") && !params.contains("example-secret"),
"should keep the raw value out of the message and params: {message} {params}"
);
}With both changes applied, clippy-fastly and test-fastly pass, and the same config prints:
[ts] typed app-config failed validation: rewrite.include_domains: rewrite.include_domains[1] must be a host name with an optional leading `*.`, a dotted-quad IPv4 address or a bracketed IPv6 address (character outside a-z, 0-9, `-`, `_` and `.`)
This has to be applied by hand: the validator and the test sit in different hunks, and the validator change alone fails the current test.
| /// Trims and lowercases host patterns in place. | ||
| /// | ||
| /// Empty and bare `*` entries in `exclude_domains` are dropped with a | ||
| /// warning: neither can match a host, so dropping them changes no behavior. |
There was a problem hiding this comment.
⛏ nitpick: The normalize doc still describes only the empty and * drop. Two facts about the new warnings belong here:
- Positions are counted before that drop, which is what makes them match the operator's TOML.
ts config pushstores the normalized list, so an edge log can show a lower position thants config validatedoes when empty or*entries came first.
The first point also has no test. Moving the inert_exclude_domain_warnings call below the filter leaves every test passing (mutation run), because rewrite_exclude_domains_warnings_omit_raw_values calls the helper directly instead of going through normalize. To pin the ordering, normalize could get its warnings from a helper that also does the filtering, so one test can check both.
| /// Trims and lowercases host patterns in place. | |
| /// | |
| /// Empty and bare `*` entries in `exclude_domains` are dropped with a | |
| /// warning: neither can match a host, so dropping them changes no behavior. | |
| /// Trims and lowercases host patterns in place. | |
| /// | |
| /// Empty and bare `*` entries in `exclude_domains` are dropped with a | |
| /// warning: neither can match a host, so dropping them changes no behavior. | |
| /// Other entries that no URL host can match are kept and reported by | |
| /// position, counted before that drop; see | |
| /// [`Self::inert_exclude_domain_warnings`]. `ts config push` stores the | |
| /// normalized list, so positions logged at the edge can sit lower than the | |
| /// ones `ts config validate` prints when empty or `*` entries came first. |
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!)