# javascript: URI scheme validation bypass

URLs reaching href, src, iframe or OS handlers without a parsed-scheme allowlist at the sink

_Area: Web injection and code execution. Seen in 29 disclosed reports; 18 confirmed bounties, median $1,700, range $300-$12,500; latest 2026-06-18. Updated 2026-10-23._

## How it works
Code that places a URL into a navigation sink (href, iframe src, window.open, OS handler) assumes a string check proves the URL is safe. The check is usually a substring match ("contains https:"), a blocklist of the literal `javascript:`, or a client-side-only filter. A URI can satisfy that check while the browser still parses it as a `javascript:` scheme, so script runs in the trusting origin. The broken assumption: string inspection equals scheme enforcement. In E1 the receiver only confirmed the URI string appeared in the message body, which a `javascript:` URI embedding an `https:` string satisfies.

## Where it shows up
- URL from an attacker-selectable source (third-party API metadata, rich previews, query params) used directly as `href`/`src`/`window.open`/iframe target [E1](https://bugbountylibrary.com/r/blog%3Aweizman-github-io-2020-02-14-whatsapp-vuln.md)[E3](https://bugbountylibrary.com/r/h1%3A1887917.md)[E4](https://bugbountylibrary.com/r/bc%3A5db6b239-491e-457c-b7cc-3246d8e2aed7.md)
- Validation that only checks a substring is present, or that the URI appears in the body [E1](https://bugbountylibrary.com/r/blog%3Aweizman-github-io-2020-02-14-whatsapp-vuln.md)
- Scheme filtering done only client-side, with the API accepting forged schemes [E8](https://bugbountylibrary.com/r/h1%3A1930763.md)
- Markdown/rich-text renderers that do not sanitize link or image targets [E6](https://bugbountylibrary.com/r/h1%3A2509022.md)
- Privileged internal pages (opera:, internal:) rendering user-stored links [E2](https://bugbountylibrary.com/r/blog%3Ablogs-opera-com-security-2021-09-bug-bounty-guest-post-local-file-read-via-stored-xss-in-the-opera-browser.md)[E7](https://bugbountylibrary.com/r/h1%3A1438028.md)
- UI that lets a user pick which stored document field becomes an anchor href [E5](https://bugbountylibrary.com/r/h1%3A846905.md)

## How to review
Find every sink that consumes a URL and trace its source. The invariant: the URL must be parsed with a real URL parser (`new URL()`, `URL.parse`), and the resulting scheme must be on an explicit allowlist (https/http) before use, enforced server-side and again at render. Reject on parse failure and on any scheme not on the list; never rely on substring or blocklist matching. Parsing, not string normalization, is what decides the scheme; normalize case, whitespace and control characters only as input to the parser, and reject protocol-relative (`//host`) and encoded forms. Allowlisting must happen at the sink, not upstream, since a filter elsewhere does not protect a sink that has none [E3](https://bugbountylibrary.com/r/h1%3A1887917.md)[E7](https://bugbountylibrary.com/r/h1%3A1438028.md). `data:`/`blob:` can also execute in some sinks (iframe), so allowlist each sink's schemes separately. For OS handlers, confirm an allowlist before `openUrl`. For privileged pages, confirm user data is never rendered as a link [E2](https://bugbountylibrary.com/r/blog%3Ablogs-opera-com-security-2021-09-bug-bounty-guest-post-local-file-read-via-stored-xss-in-the-opera-browser.md)[E7](https://bugbountylibrary.com/r/h1%3A1438028.md).

## Variants seen
- `javascript:` URI embedding an `https:` string to pass a body check [E1](https://bugbountylibrary.com/r/blog%3Aweizman-github-io-2020-02-14-whatsapp-vuln.md)
- Stored `javascript:` link in a privileged internal scheme page [E2](https://bugbountylibrary.com/r/blog%3Ablogs-opera-com-security-2021-09-bug-bounty-guest-post-local-file-read-via-stored-xss-in-the-opera-browser.md)
- Third-party API `url` field used as iframe src [E3](https://bugbountylibrary.com/r/h1%3A1887917.md)
- Query param concatenated into iframe src without scheme validation [E4](https://bugbountylibrary.com/r/bc%3A5db6b239-491e-457c-b7cc-3246d8e2aed7.md)
- Stored `javascript://` with encoded newline rendered as anchor href [E5](https://bugbountylibrary.com/r/h1%3A846905.md)
- Markdown image/link target with `javascript:` in an AI chat greeting [E6](https://bugbountylibrary.com/r/h1%3A2509022.md)
- Restore-URL path with no scheme validation on a privileged origin [E7](https://bugbountylibrary.com/r/h1%3A1438028.md)
- Client-side-only link validation bypassed by calling the API directly; draft/scheduled renderer skips sanitization [E8](https://bugbountylibrary.com/r/h1%3A1930763.md)

## Why fixes fail
Substring filters miss embedded/encoded forms [E1](https://bugbountylibrary.com/r/blog%3Aweizman-github-io-2020-02-14-whatsapp-vuln.md); client-side-only checks are bypassed by calling the API directly [E8](https://bugbountylibrary.com/r/h1%3A1930763.md); some sinks have no validation at all, so a filter elsewhere does not help [E3](https://bugbountylibrary.com/r/h1%3A1887917.md)[E7](https://bugbountylibrary.com/r/h1%3A1438028.md); draft/scheduled edit views render through a different pipeline than the published renderer, skipping sanitization [E8](https://bugbountylibrary.com/r/h1%3A1930763.md).

## Impact and severity
Session hijack, PII theft, and persistent XSS [E1](https://bugbountylibrary.com/r/blog%3Aweizman-github-io-2020-02-14-whatsapp-vuln.md)[E2](https://bugbountylibrary.com/r/blog%3Ablogs-opera-com-security-2021-09-bug-bounty-guest-post-local-file-read-via-stored-xss-in-the-opera-browser.md)[E3](https://bugbountylibrary.com/r/h1%3A1887917.md)[E6](https://bugbountylibrary.com/r/h1%3A2509022.md). E1's local file read stems from the outdated Electron/Chromium client, not the URI itself. Severity rises with privileged context (internal scheme, desktop client, admin view) and persistence. A strict CSP does not cover `javascript:` URLs in iframe src on affected Chromium versions [E5](https://bugbountylibrary.com/r/h1%3A846905.md), so policy alone is not a backstop.

## Evidence
- E1: https://bugbountylibrary.com/r/blog%3Aweizman-github-io-2020-02-14-whatsapp-vuln.md
- E2: https://bugbountylibrary.com/r/blog%3Ablogs-opera-com-security-2021-09-bug-bounty-guest-post-local-file-read-via-stored-xss-in-the-opera-browser.md
- E3: https://bugbountylibrary.com/r/h1%3A1887917.md
- E4: https://bugbountylibrary.com/r/bc%3A5db6b239-491e-457c-b7cc-3246d8e2aed7.md
- E5: https://bugbountylibrary.com/r/h1%3A846905.md
- E6: https://bugbountylibrary.com/r/h1%3A2509022.md
- E7: https://bugbountylibrary.com/r/h1%3A1438028.md
- E8: https://bugbountylibrary.com/r/h1%3A1930763.md
