fix(toolbar): match authorized URLs by exact origin#61020
Conversation
| return true | ||
| } | ||
| // www-equivalence: compare hostnames with www. stripped | ||
| return stripWww(authorizedUrlParsed.hostname) === hostNormalized |
There was a problem hiding this comment.
Protocol bypass in www-equivalence fallback allows HTTP origins to match HTTPS-only authorized entries
In the updated checkUrlIsAuthorized, the www-equivalence fallback (line 223) compares only stripped hostnames — it never compares protocols. As a result, if https://example.com is in the authorized list, the check also passes for http://example.com and http://www.example.com:
- Exact-origin check (
protocol + '//' + host === urlWithoutPath) correctly fails for mismatched protocols. - Fallback:
stripWww(authorizedUrlParsed.hostname) === hostNormalizedsucceeds because both hostnames normalize toexample.com, ignoring the protocol difference.
This is directly exercised in iframedToolbarBrowserLogic.ts:256:
if (!values.checkUrlIsAuthorized(e.origin)) { ... }An attacker who can make the browser send a postMessage from an HTTP version of an authorized domain (trivially achievable via MITM on HTTP traffic) bypasses the origin guard entirely and can inject toolbar messages (PH_TOOLBAR_NAVIGATED, PH_TOOLBAR_INIT, PH_NEW_ACTION_CREATED) into the PostHog toolbar browser. While the individual message payloads are low-value, this makes the postMessage origin gate meaningless for any authorized HTTPS site whose HTTP counterpart is reachable or spoof-able.
Prompt To Fix With AI
In `checkUrlIsAuthorized`, the www-equivalence fallback on line 223 must also require the protocols to match before returning true. Change:
```ts
return stripWww(authorizedUrlParsed.hostname) === hostNormalized
```
to:
```ts
return (
authorizedUrlParsed.protocol === parsedUrl.protocol &&
stripWww(authorizedUrlParsed.hostname) === hostNormalized
)
```
This ensures that `http://example.com` cannot match an authorized entry of `https://example.com`, while still allowing `https://www.example.com` ↔ `https://example.com` equivalence. Add a test case to `checkUrlIsAuthorized` in `authorizedUrlListLogic.test.ts`:
```ts
{ url: 'http://example.com', authorized: ['https://example.com'], expected: false },
{ url: 'http://www.example.com', authorized: ['https://example.com'], expected: false },
{ url: 'https://www.example.com',authorized: ['https://example.com'], expected: true },
{ url: 'https://example.com', authorized: ['https://www.example.com'], expected: true },
```Severity: medium | Confidence: 75%
|
Size Change: 0 B Total Size: 80.9 MB ℹ️ View Unchanged
|
|
Reviews (1): Last reviewed commit: "fix(toolbar): match authorized URLs by e..." | Re-trigger Greptile |
Problem
checkUrlIsAuthorized(shared by the toolbar, heatmaps, web analytics, and the postMessage origin check in the iframed toolbar browser) decided whether a URL belongs to a project's authorized list using a loose substring check plus an unanchored wildcard regex. As a result a URL could be considered authorized just by overlapping an authorized entry's string:https://example.com.evil.commatchedhttps://example.com(suffix)https://app.example.com.evil.commatchedhttps://*.example.com(the wildcard regex had no end anchor)https://example.comatchedhttps://example.com(prefix/substring)In each case the actual registrable domain is different from — and not controlled by the owner of — the authorized one.
Changes
Rewrite the matching so each authorized entry is evaluated exactly once:
https://*.example.com,http://localhost:*) are compiled to a regex anchored with^…$, so a*can no longer match a suffix of an unrelated origin.protocol://host) instead of substring, so a different domain can't be authorized by being a prefix/substring of an entry.Existing behavior is preserved: path is ignored,
www.equivalence still holds in both directions, and subdomain/port wildcards still match as before.How did you test this code?
I'm an agent (Claude Code). Automated tests only — no manual QA:
checkUrlIsAuthorizedtest block inauthorizedUrlListLogic.test.tscovering the legitimate matches (exact, path,www.both directions, subdomain wildcard, multi-level subdomain, port wildcard) and the suffix / prefix / unanchored-wildcard cases that must be rejected. Written test-first: the rejection cases failed against the old implementation and pass after the change.authorizedUrlListLogicsuite (55 tests, incl. existingfilterNotAuthorizedUrlscoverage) and the heatmaps logic suite — all green.🤖 Agent context
Authored by Claude Code as a follow-up to a separate change that hardened the site-preview iframe. While verifying that the site-preview authorization gate was sound, the underlying
checkUrlIsAuthorizedmatcher was found to accept origins that overlap an authorized entry's string. This PR tightens the matcher itself, which also benefits the other callers (notably thepostMessageevent-origin check iniframedToolbarBrowserLogic). Approach: kept the wildcard pattern construction identical and only added anchoring, and replaced the substring comparison with an origin comparison while preserving the existingwww.-equivalence semantics to avoid regressing legitimate matches.