Back to Blog
critical SEVERITY8 min read

How Server-Side Request Forgery happens in Browser Extensions and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability in `offscreen.js` allowed attackers to supply malicious feed URLs that the browser extension would fetch without validation, potentially exposing internal network services including cloud metadata endpoints. The fix introduces a dedicated `validateFeedUrl` utility and disables automatic redirect following, closing the attack vector before requests leave the extension. This kind of vulnerability is especially dangerous in browser extensions becau

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in a browser extension's `offscreen.js` and `rssManager.js` files, where user-controlled RSS feed URLs were fetched without any validation. An attacker who could influence the stored feed URL could redirect the extension's fetch to internal network resources such as `http://169.254.169.254/` (AWS metadata). The fix adds a `validateFeedUrl` call (imported from `modules/utils/urlSafety.js`) before every fetch, and sets `redirect: 'manual'` to prevent open-redirect chains from bypassing the check.

Vulnerability at a Glance

cweCWE-918
fixAdded `validateFeedUrl` pre-flight check and `redirect: 'manual'` option before every outbound fetch
riskExtension fetches attacker-controlled URLs, exposing internal network services and cloud metadata
languageJavaScript (Browser Extension / Node.js)
root cause`fetchRssFeed()` in `rssManager.js` passed user-supplied URLs directly to `fetch()` with no allowlist or scheme validation
vulnerabilityServer-Side Request Forgery (SSRF)

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 schemefile://, ftp://, and http:// are all accepted alongside https://
  • 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.


Prevention & Best Practices

URL Validation for Fetch-Based Features

Any time user-supplied or user-influenced data flows into a fetch(), XMLHttpRequest, or similar network call, apply these controls:

1. Validate scheme first

function validateFeedUrl(url) {
    let parsed;
    try {
        parsed = new URL(url);
    } catch {
        return 'Invalid URL format';
    }
    if (parsed.protocol !== 'https:') {
        return 'Only HTTPS feed URLs are permitted';
    }
    // ... additional checks
}

2. Block private and link-local IP ranges

After scheme validation, resolve the hostname and check against RFC 1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback (127.0.0.0/8), and link-local (169.254.0.0/16). In a browser extension context you can use chrome.dns.resolve() or a pre-fetch DNS lookup to check the resolved IP before fetching.

3. Always set redirect: 'manual'

When fetching user-supplied URLs, disable automatic redirect following. If your use case requires following redirects, re-validate each redirect target before following.

4. Apply the validation as close to the source as possible

The fix validates in fetchRssFeed() — the single function responsible for all outbound RSS fetches. This is the right chokepoint. Avoid relying on callers to pre-validate; validate at the sink.

For Browser Extension Developers Specifically

  • Declare the minimum necessary host_permissions. If your extension only needs to fetch RSS feeds, restrict to known feed domains rather than <all_urls>.
  • Treat extension storage as untrusted input. Stored values can be modified by sync, by other extensions with storage access, or by bugs elsewhere in your codebase.
  • Audit all fetch() calls in offscreen documents and service workers — these are high-privilege contexts.

Detection with Static Analysis

This vulnerability class (tainted URL flowing to fetch()) is well-supported by static analysis tools:

  • Semgrep: Rules in the javascript.browser.security.ssrf namespace can trace message-passing sources to fetch sinks.
  • CodeQL: The js/request-forgery query covers this pattern.
  • ESLint plugins: eslint-plugin-security flags dynamic fetch calls with non-literal URLs.

Relevant standards:
- OWASP SSRF Prevention Cheat Sheet
- CWE-918: Server-Side Request Forgery


Key Takeaways

  • fetchRssFeed() was the single unfenced gate — every outbound RSS request flowed through it, making it the perfect — and correct — place to add the validateFeedUrl check.
  • 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.js through esbuild was required for the new urlSafety.js import 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.storage with 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() in modules/rssManager.js
  • Sink: fetch(feedUrl, { signal: controller.signal }) at the unguarded call site in rssManager.js (around line 610 before the fix), reached through the offscreen document context in offscreen.js:140
  • Missing control: No URL scheme validation, no private IP range blocklist, and no redirect: 'manual' policy — the URL was passed verbatim to fetch() with full redirect-following enabled
  • CWE: CWE-918 — Server-Side Request Forgery (SSRF)
  • Fix: Added validateFeedUrl pre-flight check imported from modules/utils/urlSafety.js and set redirect: '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.


References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker can cause an application (or in this case, a browser extension) to make HTTP requests to an unintended destination, such as internal network services or cloud metadata endpoints, by supplying a malicious URL.

How do you prevent SSRF in JavaScript browser extensions?

Validate every URL before fetching it — enforce an allowlist of schemes (https only), block private IP ranges (RFC 1918, link-local), and set `redirect: 'manual'` to prevent redirect chains from bypassing your validation.

What CWE is Server-Side Request Forgery?

SSRF is classified as CWE-918: Server-Side Request Forgery.

Is checking for `http://` vs `https://` enough to prevent SSRF?

No. Scheme checking alone is insufficient. Attackers can use HTTPS URLs that resolve to internal IPs, DNS rebinding, or redirect chains. You must also validate resolved IP ranges and disable automatic redirects.

Can static analysis detect SSRF?

Yes. Tools like Semgrep can trace tainted data from message-passing handlers to `fetch()` calls and flag unvalidated URL sinks. Orbis AppSec's multi-agent AI scanner detected exactly this pattern in `offscreen.js`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #193

Related Articles

high

How SSRF and Credential Leakage happens in Node.js axios and how to fix it

CVE-2025-27152 is a high-severity vulnerability in axios versions prior to 1.8.2 that allows Server-Side Request Forgery (SSRF) and credential leakage when absolute URLs are passed in requests. By upgrading from the vulnerable `^1.7.4` range (which resolved to `1.7.9`) to the pinned `1.8.2`, the attack surface for intercepting or redirecting authenticated HTTP requests is eliminated. Any Node.js application that passes user-influenced URLs to axios is potentially affected.

critical

How Server-Side Request Forgery (SSRF) happens in JavaScript playlist importers and how to fix it

A critical SSRF vulnerability in `js/cd-player/playlist-importer.js` allowed attacker-controlled URLs from third-party Meting APIs to be stored and later fetched by users' browsers, potentially exposing internal network resources. The fix introduces an `isSafeUrl()` validation function that enforces HTTPS-only URLs before any track audio or cover art URL is accepted into the application. This change closes the attack path without altering the normal playlist import workflow.

critical

How Server-Side Request Forgery (SSRF) happens in JavaScript and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `functions/stream/createProxyResponse.js`, where the `location` parameter was passed directly to `fetch()` without any URL validation. This allowed attackers to weaponize the proxy function to reach internal network resources, cloud metadata endpoints, and arbitrary external services. The fix adds protocol validation using the `URL` constructor before any fetch operation is performed.

critical

How Unvalidated Update URLs Happen in Node.js Agent Updaters and How to Fix Them

A critical vulnerability in `agent/src/updater.js` allowed an attacker who could modify the agent's configuration to redirect software update downloads to an attacker-controlled server, enabling remote code execution via a crafted tarball. The fix introduces strict hostname validation — including private network awareness — so the updater only fetches from trusted origins. This kind of supply-chain attack vector is easy to overlook but catastrophic in production agent deployments.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity vulnerability (CVE-2026-69192) was discovered in the ip-address library version 10.1.0, where inconsistent IP address parsing could lead to Server-Side Request Forgery (SSRF) and trust-boundary bypass attacks. The vulnerability was fixed by upgrading ip-address from 10.1.0 to 10.3.1 in the gateway-workflow-dispatcher-v2.js component, preventing attackers from bypassing IP validation checks and accessing internal resources.

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr