The Vulnerability: Unvalidated Feed URLs in a Browser Extension's Offscreen Context
The offscreen.js script in this browser extension handles a specific and powerful job: it runs in a dedicated offscreen document — an isolated page that Chrome extensions use to perform DOM operations and network requests outside the main service worker. Because it communicates via message passing and can reach arbitrary URLs, what goes into its fetch() calls matters enormously.
In modules/rssManager.js, the fetchRssFeed() function accepted a feedUrl parameter sourced from extension storage or incoming messages and passed it directly to the browser's fetch() API — no scheme check, no IP range validation, no redirect policy. This is a textbook Server-Side Request Forgery (SSRF) vulnerability, and in the context of a browser extension running on a user's machine, the blast radius extends to every service reachable from that device.
The Vulnerability Explained
The Vulnerable Code Pattern
Before the fix, fetchRssFeed in rssManager.js looked like this (simplified to the critical path):
async function fetchRssFeed(feedUrl) {
try {
let parsedData;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
// ❌ feedUrl is passed directly — no validation
const response = await fetch(feedUrl, { signal: controller.signal });
// ... parse response
}
}
}
The feedUrl value flows from extension storage or a message-passing handler into fetch() with zero sanitization. There is no check for:
- URL scheme —
file://,ftp://, andhttp://are all accepted alongsidehttps:// - Destination IP — private ranges (
10.x.x.x,192.168.x.x,172.16-31.x.x) and link-local addresses (169.254.x.x) are not blocked - Redirect following — the default
redirect: 'follow'behavior means a seemingly-safe HTTPS URL can chain through a redirect to an internal resource
How an Attacker Exploits This
An attacker who can write to the extension's storage (e.g., via a compromised sync account, a malicious web page that exploits a separate XSS, or direct manipulation of the RSS subscription list) can plant a feed URL like:
http://169.254.169.254/latest/meta-data/iam/security-credentials/
When the extension's background script calls fetchRssFeed() on the next refresh cycle, it dutifully fetches the AWS Instance Metadata Service endpoint. The response — containing IAM role credentials — is then processed by the extension's RSS parser. Even if the parser fails gracefully, the HTTP request has already been made and the credentials may have been logged or exfiltrated.
Other high-value targets on a typical developer's machine include:
| Target | URL |
|---|---|
| AWS metadata | http://169.254.169.254/latest/meta-data/ |
| Local Kubernetes API | http://localhost:8001/api/v1/secrets |
| Docker daemon | http://localhost:2375/containers/json |
| Corporate intranet | http://10.0.0.1/admin |
Because the fetch originates from the browser extension context (not a remote server), traditional network perimeter controls offer no protection here.
Why the Offscreen Context Makes This Worse
Chrome's offscreen documents were introduced to let extensions perform tasks like audio playback and DOM parsing in an isolated context. But "isolated" refers to the rendering environment — the network access is still subject to the same permissions as the extension. An extension with broad host_permissions can reach internal addresses. The offscreen document is a convenient fetch proxy, and without URL validation it becomes an open one.
The Fix
The fix is clean, targeted, and applied at exactly the right layer. Three coordinated changes close the vulnerability:
1. Pre-flight URL Validation in fetchRssFeed()
// modules/rssManager.js
// NEW import at the top of the file
import { validateFeedUrl as checkUrlSafety } from './utils/urlSafety.js';
async function fetchRssFeed(feedUrl) {
// ✅ Validate before any network activity
const urlError = checkUrlSafety(feedUrl);
if (urlError) {
throw new Error(urlError);
}
try {
let parsedData;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
// ✅ redirect: 'manual' prevents redirect-chain bypasses
const response = await fetch(feedUrl, { signal: controller.signal, redirect: 'manual' });
The checkUrlSafety() call (aliased from validateFeedUrl) runs synchronously before the AbortController is even created. If the URL fails validation, an error is thrown immediately and no network request is ever initiated. This is the correct place to validate: as early as possible, before any side effects.
2. redirect: 'manual' Stops Redirect-Chain Bypasses
// Before
const response = await fetch(feedUrl, { signal: controller.signal });
// After
const response = await fetch(feedUrl, { signal: controller.signal, redirect: 'manual' });
This single option change is subtle but critical. Without it, an attacker could register an HTTPS feed URL that returns a 301 Redirect to http://169.254.169.254/. The browser would follow the redirect transparently, and validateFeedUrl — which only saw the original HTTPS URL — would have been bypassed. With redirect: 'manual', the extension receives an opaque redirect response and never follows the chain.
3. Build Pipeline Fix Ensures the Validated Code Ships
# Before: offscreen.js was copied as a raw module (not bundled)
PROD_STATIC_FILES = \
icons \
lib \
_locales \
offscreen.html \
offscreen.js # ← raw, unbundled source
# After: offscreen.js is bundled via esbuild like other scripts
build-prod:
@npx esbuild offscreen.js --bundle --minify --outfile=$(PROD_BUILD_DIR)/offscreen.js
@sed -i.bak 's/type="module" //' $(PROD_BUILD_DIR)/offscreen.html
This change is easy to overlook but essential. Previously, offscreen.js was shipped as a raw ES module — meaning its import statements (including the new import { validateFeedUrl } from urlSafety.js) would not resolve correctly in the production build. By moving it through esbuild, the entire dependency graph including the new validation utility is bundled and minified into the production artifact. The fix would have been inert without this Makefile change.
Key Takeaways
fetchRssFeed()was the single unfenced gate — every outbound RSS request flowed through it, making it the perfect — and correct — place to add thevalidateFeedUrlcheck.redirect: 'manual'is not optional when validating user URLs — a URL that passes scheme and IP validation can still redirect to a forbidden destination; disabling auto-redirect closes this bypass.- The Makefile change was load-bearing — bundling
offscreen.jsthroughesbuildwas required for the newurlSafety.jsimport to resolve in production; a security fix that doesn't ship is not a fix. - Extension storage is attacker-influenced data — treat any value read from
chrome.storagewith the same skepticism as a value from an HTTP request parameter. - Offscreen documents inherit extension network permissions — their apparent isolation is rendering-only; a fetch from an offscreen document can reach the same internal addresses as any other extension context.
How Orbis AppSec Detected This
- Source: User-controlled feed URL stored in extension storage and passed via message passing into
fetchRssFeed()inmodules/rssManager.js - Sink:
fetch(feedUrl, { signal: controller.signal })at the unguarded call site inrssManager.js(around line 610 before the fix), reached through the offscreen document context inoffscreen.js:140 - Missing control: No URL scheme validation, no private IP range blocklist, and no
redirect: 'manual'policy — the URL was passed verbatim tofetch()with full redirect-following enabled - CWE: CWE-918 — Server-Side Request Forgery (SSRF)
- Fix: Added
validateFeedUrlpre-flight check imported frommodules/utils/urlSafety.jsand setredirect: 'manual'on all outbound feed fetches
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
SSRF vulnerabilities in browser extensions are underappreciated precisely because developers think of SSRF as a server-side problem. But a browser extension that fetches user-supplied URLs is acting as a network proxy — and without validation, it's an open one. The offscreen.js vulnerability demonstrated how a single missing guard in fetchRssFeed() could turn an RSS reader feature into a tool for probing internal networks and cloud metadata services.
The fix is a model for how to address this class of issue: validate at the chokepoint (fetchRssFeed), disable redirect following, and ensure the fix actually ships by updating the build pipeline. If you maintain a browser extension that fetches user-supplied URLs, audit every fetch() call today.