Description
In crates/trusted-server-core/src/integrations/google_tag_manager.rs, build_target_url consults the validated container_id only when the request carries no query string:
if let Some(query) = req.uri().query() {
target_url = format!("{}?{}", target_url, query);
} else if path.ends_with("/gtm.js") {
target_url = format!("{}?id={}", target_url, self.config.container_id);
}
Any query string therefore wins, and the response is re-served from the publisher's own origin as application/javascript with Cache-Control: public, max-age=<cache_max_age>. The same applies to /gtag/js, which answers 200 for tag ids that do not exist, so upstream cannot be relied on to reject an unknown one.
Related problems in the same file:
upstream_url is validated only as a URL. It may be plaintext, may embed a username and password, and may carry a wildcard host — and it is handed to the browser in the redirect for an unconfigured tag, so anything in it is disclosed. The proxy fetch is also unpinned: follow_redirects defaults on and an empty allowed_domains means any host.
- Responses passed through unmodified may carry
Set-Cookie, Strict-Transport-Security or Clear-Site-Data, all of which act against the serving origin.
GTM_URL_PATTERN matched on host alone, so any path on the Google hosts was rewritten to a first-party URL — but only five paths have routes, so the rest became first-party 404s. On the live GTM script this produced three broken rewrites, two of them landing inside minified JavaScript expressions.
Expected behavior
gtm.js serves the configured container regardless of what the request asks for.
gtag/js serves only tag ids the operator has configured; any other id is not served from this origin.
- The upstream fetch is pinned to HTTPS and to the configured hosts.
- Passed-through responses cannot set state against the publisher's origin.
- Only routed paths are rewritten first-party.
Affected area
Integrations (prebid, lockr, permutive, etc.)
Done when
Description
In
crates/trusted-server-core/src/integrations/google_tag_manager.rs,build_target_urlconsults the validatedcontainer_idonly when the request carries no query string:Any query string therefore wins, and the response is re-served from the publisher's own origin as
application/javascriptwithCache-Control: public, max-age=<cache_max_age>. The same applies to/gtag/js, which answers200for tag ids that do not exist, so upstream cannot be relied on to reject an unknown one.Related problems in the same file:
upstream_urlis validated only as a URL. It may be plaintext, may embed a username and password, and may carry a wildcard host — and it is handed to the browser in the redirect for an unconfigured tag, so anything in it is disclosed. The proxy fetch is also unpinned:follow_redirectsdefaults on and an emptyallowed_domainsmeans any host.Set-Cookie,Strict-Transport-SecurityorClear-Site-Data, all of which act against the serving origin.GTM_URL_PATTERNmatched on host alone, so any path on the Google hosts was rewritten to a first-party URL — but only five paths have routes, so the rest became first-party 404s. On the live GTM script this produced three broken rewrites, two of them landing inside minified JavaScript expressions.Expected behavior
gtm.jsserves the configured container regardless of what the request asks for.gtag/jsserves only tag ids the operator has configured; any other id is not served from this origin.Affected area
Integrations (prebid, lockr, permutive, etc.)
Done when
idcannot change which containergtm.jsservesgtag/jsis limited to configured tag ids, with an operator-visible upgrade noteupstream_urlmust be HTTPS with a literal host and no credentialsSet-Cookie/ HSTS /Clear-Site-Dataare stripped from passthrough responses