Repository navigation
Lockr script guard rewrites any script URL containing aim.loc.kr to the Lockr SDK #1199
Description
Description
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):
const lower = url.toLowerCase();
// Check for aim.loc.kr domain
if (lower.includes('aim.loc.kr')) {
return true;
}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. - Update Lockr integration to use dedicated trust-server SDK #395 (
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. - 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, resetGuardState as resetGptGuard, } from '../../../src/integrations/gpt/script_guard'; import { installLockrGuard, resetGuardState as resetLockrGuard, } 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. function insertScript(url: string): string { const script = document.createElement('script'); script.src = url; document.head.appendChild(script); const src = script.getAttribute('src') ?? ''; script.remove(); return src; } 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.tsObserved at
a4e01eb55, all three cases fail:Expected: "http://localhost:3000/integrations/gpt/tag/js/gpt.js?ref=aim.loc.kr" Received: "http://localhost:3000/integrations/lockr/sdk" Expected: "https://ads.example.com/loader.js?page=https%3A%2F%2Fpublisher.example.com%2F%3Futm_source%3Daim.loc.kr" Received: "http://localhost:3000/integrations/lockr/sdk" Expected: "https://aim.loc.kr.example.com/loader.js" Received: "http://localhost:3000/integrations/lockr/sdk"
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:
| URL | Browser guards that claim it | Rust matcher |
|---|---|---|
https://aim.loc.kr/identity-lockr-trust-server.js |
lockr | lockr: true |
https://securepubads.g.doubleclick.net/tag/js/gpt.js?ref=aim.loc.kr |
gpt, lockr | lockr: false |
https://www.googletagmanager.com/gtm.js?id=GTM-TEST&ref=aim.loc.kr |
google_tag_manager, lockr | lockr: false |
https://ads.example.com/loader.js?page=...%3Daim.loc.kr (full URL above) |
lockr | lockr: false |
https://aim.loc.kr/some-other-script.js |
lockr | lockr: false |
https://aim.loc.kr.example.com/loader.js |
lockr | lockr: false |
https://aim.loc.kr.example.com/identity-lockr.js |
lockr | lockr: true |
https://static.example.com/aim.loc.kr/identity-lockr.js |
lockr | lockr: true |
https://aim.loc.kr/identity-lockr-trust-server.js?v=2 |
lockr | lockr: false |
https://cdn.permutive.com/abc123-web.js |
permutive | permutive: true |
https://cdn.permutive.com.example.com/abc123-web.js |
permutive | permutive: true |
https://static.example.com/mirror/cdn.permutive.com/app-web.js |
permutive | permutive: true |
https://cdn.permutive.com/abc123-web.js?v=2 |
none | permutive: false |
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 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.- 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, sogptsorts beforelockrand GPT wins today. - The GTM URL. Both guards claim it.
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
- 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.krand 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:
containschecks plusends_with(".js")on the full URL (integrations/lockr.rs:102-104). A query string after.jsdefeats the suffix check. - Browser and server Permutive:
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:- 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
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:
const LOCKR_SDK_HOSTS = new Set(['aim.loc.kr', 'identity.loc.kr']);
function isLockrSdkUrl(url: string): boolean {
if (!url) return false;
let parsed: URL;
try {
parsed = new URL(url, window.location.href);
} catch {
return false;
}
const path = parsed.pathname.toLowerCase();
return (
LOCKR_SDK_HOSTS.has(parsed.hostname) && path.includes('identity-lockr') && path.endsWith('.js')
);
}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.krwith 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.kror 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.srcand nativesetAttribute. - 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.
Affected area
Integrations (prebid, lockr, permutive, etc.)
Version
main at a4e01eb
Related
- Lockr Integration #123: Lockr integration, which introduced both Lockr matchers.
- Update Lockr integration to use dedicated trust-server SDK #394 and Update Lockr integration to use dedicated trust-server SDK #395: moved Lockr to the trust-server SDK and tightened only the Rust matcher.
- Centralize DOM insertion guards #460: centralized the DOM insertion guards behind the first-claim-wins dispatcher.
- Fix for permutive sdk not proxing due to async script loaders #200: Permutive browser guard for async script loaders.
- The origin host is matched as a substring, so a third party host is rewritten to the proxy #1119 (open): the same class of bug in origin-host rewriting, where a host is matched as a substring.
- Define integrations crate split and ordered configuration #1194 (open): design PR whose review found this. It proposes configured-order dispatch, which would flip the GTM case.
Activity
Include milestone issue #1199 PR #1405 in RC