You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Unless given in full, JS paths below are relative to crates/trusted-server-js/lib/src/ and Rust paths to crates/trusted-server-core/src/.
The browser Lockr guard decides whether a dynamically inserted script, or a <link rel="preload|prefetch" as="script">, is the Lockr SDK by searching the whole URL string (integrations/lockr/script_guard.ts:22-42):
constlower=url.toLowerCase();// Check for aim.loc.kr domainif(lower.includes('aim.loc.kr')){returntrue;}
Any script whose URL contains aim.loc.kr anywhere is claimed: in the query string, in the path, or at the start of a different host. The guard then replaces the element's src with {origin}/integrations/lockr/sdk (shared/script_guard.ts:68-70, :85-98), and that route serves the configured Lockr SDK. The original script never loads.
The other DOM insertion guards match on the host. GPT compares the parsed hostname (integrations/gpt/script_guard.ts:84-87), and so does Sourcepoint (integrations/sourcepoint/script_guard.ts:33-35). GTM and DataDome use patterns anchored at the start of the URL (integrations/google_tag_manager/script_guard.ts:18-19, integrations/datadome/script_guard.ts:18).
The server-side Lockr matcher is stricter, but it is also a substring test, not a host check (integrations/lockr.rs:100-105):
let lower = url.to_ascii_lowercase();(lower.contains("aim.loc.kr") || lower.contains("identity.loc.kr"))
&& lower.contains("identity-lockr")
&& lower.ends_with(".js")
The Permutive matchers have the same shape in both languages (integrations/permutive/script_guard.ts:7-15, integrations/permutive.rs:101-105): .edge.permutive.app or cdn.permutive.com anywhere in the URL, and the whole URL ending in -web.js.
History:
Lockr Integration #123 (68eb9290f, 2025-12-15) added both Lockr matchers with the logic the browser guard still has.
Centralize DOM insertion guards #460 (9ac8c95e0, 2026-03-18) moved the guards onto the shared DOM insertion dispatcher, where the first handler that claims a candidate wins.
Steps to reproduce
Save this as crates/trusted-server-js/lib/test/integrations/lockr/url_collision.test.ts:
import{afterEach,describe,expect,it}from'vitest';import{installGptGuard,resetGuardStateasresetGptGuard,}from'../../../src/integrations/gpt/script_guard';import{installLockrGuard,resetGuardStateasresetLockrGuard,}from'../../../src/integrations/lockr/script_guard';import{resetDomInsertionDispatcherForTests}from'../../../src/shared/dom_insertion_dispatcher';// Insert the way script loaders commonly do: set src, then append.functioninsertScript(url: string): string{constscript=document.createElement('script');script.src=url;document.head.appendChild(script);constsrc=script.getAttribute('src')??'';script.remove();returnsrc;}describe('Lockr guard only claims Lockr SDK URLs',()=>{afterEach(()=>{resetGptGuard();resetLockrGuard();resetDomInsertionDispatcherForTests();});it.each([['https://securepubads.g.doubleclick.net/tag/js/gpt.js?ref=aim.loc.kr','http://localhost:3000/integrations/gpt/tag/js/gpt.js?ref=aim.loc.kr',],['https://ads.example.com/loader.js?page=https%3A%2F%2Fpublisher.example.com%2F%3Futm_source%3Daim.loc.kr','https://ads.example.com/loader.js?page=https%3A%2F%2Fpublisher.example.com%2F%3Futm_source%3Daim.loc.kr',],['https://aim.loc.kr.example.com/loader.js','https://aim.loc.kr.example.com/loader.js'],])('leaves %s to its owner',(url,expected)=>{installLockrGuard();installGptGuard();expect(insertScript(url),'should not become the Lockr SDK').toBe(expected);});});
Run it:
cd crates/trusted-server-js/lib
npx vitest run test/integrations/lockr/url_collision.test.ts
I also installed all six guards (Permutive, Lockr, Sourcepoint, GTM, DataDome, GPT) and asked every registered dispatcher handler whether it claims each URL. I ran the Rust matchers on the same URLs through a test module added to a copy of the source tree:
The canonical URL of each of the six guards has exactly one claimant today. The problems are the other rows.
Which claimant wins depends on how the script is inserted:
script.src = url, then append (the common loader pattern). The GPT guard rewrites at assignment time and keeps the query string (integrations/gpt/script_guard.ts:97-106), so the new first-party URL still contains aim.loc.kr. At insertion, GPT's dispatcher handler declines, because the URL is no longer on the GPT host (:155-175). The dispatcher offers it to the next handler, and Lockr claims it. Dispatcher order does not matter on this path: the result was the Lockr SDK with the current order, with gpt forced first and with lockr forced first.
Native Element.prototype.setAttribute, then append. Both guards claim the raw URL at insertion, and the dispatcher takes the first by priority, then ID (shared/dom_insertion_dispatcher.ts:107-120). All handlers use the default priority 100, so gpt sorts before lockr and GPT wins today.
The GTM URL. Both guards claim it. google_tag_manager sorts before lockr, so GTM wins today on every insertion path. With lockr ordered before google_tag_manager, the same insertion loads the Lockr SDK. Define integrations crate split and ordered configuration #1194 proposes dispatching in configured integration order, which would allow exactly that.
Expected behavior
A guard claims only URLs on its own vendor host with its own SDK path.
A URL that mentions a vendor host in its query or path, or whose host only starts with a vendor host, is left to its real owner or left unchanged.
Every URL has at most one claimant, so dispatcher order never decides which script loads.
Browser and server matchers accept the same set of URLs.
Actual behavior
The browser Lockr guard claims any URL containing aim.loc.kr and replaces the script with the Lockr SDK.
Both Lockr matchers accept lookalike hosts and path mentions of the host, and neither checks the host itself.
Both Permutive matchers accept lookalike hosts and path mentions, and reject a real SDK URL that has a query string.
Root cause
All four matchers test substrings of the whole URL string instead of the parsed host and path:
Browser Lockr: lower.includes('aim.loc.kr') alone is enough (integrations/lockr/script_guard.ts:28).
Server Lockr: contains checks plus ends_with(".js") on the full URL (integrations/lockr.rs:102-104). A query string after .js defeats the suffix check.
Browser and server Permutive: includes/contains on the host names plus endsWith("-web.js") on the full URL (integrations/permutive/script_guard.ts:10-14, integrations/permutive.rs:103-104).
Impact
Only deployments with the Lockr integration enabled are exposed to the Lockr problem. Its browser module is in the page bundle only when the integration is enabled (crates/trusted-server-core/src/integrations/registry.rs:1182-1197).
The trigger is aim.loc.kr inside a script URL that Lockr does not own. The default GPT script URL (https://securepubads.g.doubleclick.net/tag/js/gpt.js, crates/trusted-server-core/src/integrations/gpt.rs:548-549) and a plain gtm.js?id=... URL do not contain it. It takes a crafted or unusual URL, for example:
a query parameter on a loader URL;
a third-party loader that copies the page URL into its own script URL, on a page whose URL contains the text (for example in a tracking parameter; anyone can add one to a link);
another script served from aim.loc.kr;
a host that merely starts with aim.loc.kr.
I found no such loader in this repository. Likelihood is low.
The consequence is serious when it happens. The intended script is silently replaced by the Lockr SDK, served first-party through the publisher's own proxy. If the script is a dynamically inserted GPT loader, GPT never loads and that page view shows no GPT ads. (A static GPT tag in the HTML is rewritten on the server, where the Lockr matcher does not claim it.) Nothing reports an error. The rewrite is logged only at info inside the Lockr bundle, whose logger does not follow tsjs.setConfig({ logLevel }) (see Integration IIFEs keep private core state, so Permutive segments never reach /auction #1196), so it stays hidden even while debugging.
Permutive false positives need a URL whose whole string ends in -web.js and mentions a Permutive host elsewhere. That is unlikely. The false negative is more plausible: a Permutive SDK URL with any query string, such as a cache buster, is proxied neither by the browser guard nor by the server rewriter, so it loads directly from the vendor.
The server Lockr matcher misses the SDK URL when it has a query string, so a static <script src> for it is not rewritten. The browser guard still catches it when the script is inserted dynamically.
Proposed fix
Parse the URL, compare the host exactly, and test the path instead of the whole string.
Browser Permutive: the host is cdn.permutive.com or ends with .edge.permutive.app, and the path ends with -web.js.
Rust: the same rules with url::Url, which trusted-server-core already depends on (crates/trusted-server-core/Cargo.toml:49).
Keep one URL corpus as a JSON fixture under crates/trusted-server-js/lib/test/fixtures/, with the expected owner (or none) for each URL. Use it from Vitest and from the Rust tests in lockr.rs and permutive.rs. aps-renderer-v1.json already works this way (integrations/aps.rs:2466, auction/orchestrator.rs:6642).
Compatibility:
The browser guard stops replacing non-SDK scripts on aim.loc.kr with the SDK. The server matcher already rejects them.
SDK URLs with a query string start being proxied, in the browser and on the server.
I prototyped both browser matchers in a scratch copy of a4e01eb55. Every URL in the table then had the right single claimant or none (uppercase and protocol-relative SDK URLs still matched), the test above passed, the GTM case gave the same result in both orders, and all existing Vitest tests passed.
Done when
The browser and server Lockr matchers compare the parsed host exactly and check the path, and so do both Permutive matchers.
A shared URL corpus drives the browser and Rust tests. It includes each guard's canonical URLs, other vendors' URLs that mention aim.loc.kr or a Permutive host, lookalike hosts, path mentions, and SDK URLs with query strings.
A Vitest test installs all six guards and asserts exactly one claimant for each owned URL and none for the others, inserting through both script.src and native setAttribute.
The regression test above passes.
The stale "Matches the logic from lockr.rs:79-86" and "permutive.rs:97-101" comments are replaced by a pointer to the shared corpus.
Description
Unless given in full, JS paths below are relative to
crates/trusted-server-js/lib/src/and Rust paths tocrates/trusted-server-core/src/.The browser Lockr guard decides whether a dynamically inserted script, or a
<link rel="preload|prefetch" as="script">, is the Lockr SDK by searching the whole URL string (integrations/lockr/script_guard.ts:22-42):Any script whose URL contains
aim.loc.kranywhere is claimed: in the query string, in the path, or at the start of a different host. The guard then replaces the element'ssrcwith{origin}/integrations/lockr/sdk(shared/script_guard.ts:68-70,:85-98), and that route serves the configured Lockr SDK. The original script never loads.The other DOM insertion guards match on the host. GPT compares the parsed hostname (
integrations/gpt/script_guard.ts:84-87), and so does Sourcepoint (integrations/sourcepoint/script_guard.ts:33-35). GTM and DataDome use patterns anchored at the start of the URL (integrations/google_tag_manager/script_guard.ts:18-19,integrations/datadome/script_guard.ts:18).The server-side Lockr matcher is stricter, but it is also a substring test, not a host check (
integrations/lockr.rs:100-105):The Permutive matchers have the same shape in both languages (
integrations/permutive/script_guard.ts:7-15,integrations/permutive.rs:101-105):.edge.permutive.apporcdn.permutive.comanywhere in the URL, and the whole URL ending in-web.js.History:
68eb9290f, 2025-12-15) added both Lockr matchers with the logic the browser guard still has.058442bc0, 2026-03-16) tightened only the Rust matcher. The browser guard still carries the comment "Matches the logic from lockr.rs:79-86", which no longer holds.9ac8c95e0, 2026-03-18) moved the guards onto the shared DOM insertion dispatcher, where the first handler that claims a candidate wins.Steps to reproduce
Save this as
crates/trusted-server-js/lib/test/integrations/lockr/url_collision.test.ts:Run it:
cd crates/trusted-server-js/lib npx vitest run test/integrations/lockr/url_collision.test.tsObserved at
a4e01eb55, all three cases fail:I also installed all six guards (Permutive, Lockr, Sourcepoint, GTM, DataDome, GPT) and asked every registered dispatcher handler whether it claims each URL. I ran the Rust matchers on the same URLs through a test module added to a copy of the source tree:
https://aim.loc.kr/identity-lockr-trust-server.jshttps://securepubads.g.doubleclick.net/tag/js/gpt.js?ref=aim.loc.krhttps://www.googletagmanager.com/gtm.js?id=GTM-TEST&ref=aim.loc.krhttps://ads.example.com/loader.js?page=...%3Daim.loc.kr(full URL above)https://aim.loc.kr/some-other-script.jshttps://aim.loc.kr.example.com/loader.jshttps://aim.loc.kr.example.com/identity-lockr.jshttps://static.example.com/aim.loc.kr/identity-lockr.jshttps://aim.loc.kr/identity-lockr-trust-server.js?v=2https://cdn.permutive.com/abc123-web.jshttps://cdn.permutive.com.example.com/abc123-web.jshttps://static.example.com/mirror/cdn.permutive.com/app-web.jshttps://cdn.permutive.com/abc123-web.js?v=2The canonical URL of each of the six guards has exactly one claimant today. The problems are the other rows.
Which claimant wins depends on how the script is inserted:
script.src = url, then append (the common loader pattern). The GPT guard rewrites at assignment time and keeps the query string (integrations/gpt/script_guard.ts:97-106), so the new first-party URL still containsaim.loc.kr. At insertion, GPT's dispatcher handler declines, because the URL is no longer on the GPT host (:155-175). The dispatcher offers it to the next handler, and Lockr claims it. Dispatcher order does not matter on this path: the result was the Lockr SDK with the current order, withgptforced first and withlockrforced first.Element.prototype.setAttribute, then append. Both guards claim the raw URL at insertion, and the dispatcher takes the first by priority, then ID (shared/dom_insertion_dispatcher.ts:107-120). All handlers use the default priority 100, sogptsorts beforelockrand GPT wins today.google_tag_managersorts beforelockr, so GTM wins today on every insertion path. Withlockrordered beforegoogle_tag_manager, the same insertion loads the Lockr SDK. Define integrations crate split and ordered configuration #1194 proposes dispatching in configured integration order, which would allow exactly that.Expected behavior
Actual behavior
aim.loc.krand replaces the script with the Lockr SDK.Root cause
All four matchers test substrings of the whole URL string instead of the parsed host and path:
lower.includes('aim.loc.kr')alone is enough (integrations/lockr/script_guard.ts:28).containschecks plusends_with(".js")on the full URL (integrations/lockr.rs:102-104). A query string after.jsdefeats the suffix check.includes/containson the host names plusendsWith("-web.js")on the full URL (integrations/permutive/script_guard.ts:10-14,integrations/permutive.rs:103-104).Impact
Only deployments with the Lockr integration enabled are exposed to the Lockr problem. Its browser module is in the page bundle only when the integration is enabled (
crates/trusted-server-core/src/integrations/registry.rs:1182-1197).The trigger is
aim.loc.krinside a script URL that Lockr does not own. The default GPT script URL (https://securepubads.g.doubleclick.net/tag/js/gpt.js,crates/trusted-server-core/src/integrations/gpt.rs:548-549) and a plaingtm.js?id=...URL do not contain it. It takes a crafted or unusual URL, for example:aim.loc.kr;aim.loc.kr.I found no such loader in this repository. Likelihood is low.
The consequence is serious when it happens. The intended script is silently replaced by the Lockr SDK, served first-party through the publisher's own proxy. If the script is a dynamically inserted GPT loader, GPT never loads and that page view shows no GPT ads. (A static GPT tag in the HTML is rewritten on the server, where the Lockr matcher does not claim it.) Nothing reports an error. The rewrite is logged only at
infoinside the Lockr bundle, whose logger does not followtsjs.setConfig({ logLevel })(see Integration IIFEs keep private core state, so Permutive segments never reach /auction #1196), so it stays hidden even while debugging.Permutive false positives need a URL whose whole string ends in
-web.jsand mentions a Permutive host elsewhere. That is unlikely. The false negative is more plausible: a Permutive SDK URL with any query string, such as a cache buster, is proxied neither by the browser guard nor by the server rewriter, so it loads directly from the vendor.The server Lockr matcher misses the SDK URL when it has a query string, so a static
<script src>for it is not rewritten. The browser guard still catches it when the script is inserted dynamically.Proposed fix
Parse the URL, compare the host exactly, and test the path instead of the whole string.
Browser Lockr:
Browser Permutive: the host is
cdn.permutive.comor ends with.edge.permutive.app, and the path ends with-web.js.Rust: the same rules with
url::Url, whichtrusted-server-corealready depends on (crates/trusted-server-core/Cargo.toml:49).Keep one URL corpus as a JSON fixture under
crates/trusted-server-js/lib/test/fixtures/, with the expected owner (or none) for each URL. Use it from Vitest and from the Rust tests inlockr.rsandpermutive.rs.aps-renderer-v1.jsonalready works this way (integrations/aps.rs:2466,auction/orchestrator.rs:6642).Compatibility:
aim.loc.krwith the SDK. The server matcher already rejects them.I prototyped both browser matchers in a scratch copy of
a4e01eb55. Every URL in the table then had the right single claimant or none (uppercase and protocol-relative SDK URLs still matched), the test above passed, the GTM case gave the same result in both orders, and all existing Vitest tests passed.Done when
aim.loc.kror a Permutive host, lookalike hosts, path mentions, and SDK URLs with query strings.script.srcand nativesetAttribute.Affected area
Integrations (prebid, lockr, permutive, etc.)
Version
main at a4e01eb
Related