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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #193

Related Articles

medium

How gitlab.bandit.B501 happens in Python and how to fix it

The `proverbia-scraper.py` script disabled TLS certificate verification on its `requests.get()` call and silenced the resulting security warnings, exposing the scraper to man-in-the-middle attacks. The fix removes the `verify=False` flag and the warning suppression, restoring proper certificate validation while keeping the existing 30-second timeout intact.

high

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

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict

critical

How SSRF via Vulnerable Dependency Versions Happens in Node.js and How to Fix It

A permissive semver range in `package.json` allowed npm to install axios versions vulnerable to SSRF (CVE-2024-39338). By bumping the minimum version from `^1.6.0` to `^1.7.4`, all downstream consumers of this SDK are now protected from server-side request forgery attacks. This critical fix required changing just one line in the dependency manifest.

critical

How Server-Side Request Forgery happens in Python FastAPI and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in app.py where the `/parse` and `/parse-video` endpoints accepted user-supplied URLs with only substring validation. The application checked if 'doubao.com' appeared anywhere in the URL string, allowing attackers to bypass this check and access internal services, cloud metadata endpoints, or scan the internal network. The fix implemented proper hostname parsing with an allowlist of legitimate domains.

critical

How Server-Side Request Forgery happens in Node.js maintenance scripts and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `maintenance/getImages.js`, where the `getImage()` function passed database-sourced URLs directly to `axios.get()` without any validation. An attacker who could modify the elements database could redirect these requests to internal network resources — including AWS cloud metadata endpoints — potentially exposing IAM credentials and other sensitive infrastructure data. The fix introduces a strict URL allowlist that limi

high

How SSRF via inconsistent IP address parsing happens in Node.js dependencies and how to fix it

A high-severity flaw (CVE-2026-69192) in the widely-used `ip-address` npm package meant that IP strings could be parsed inconsistently compared to the OS resolver and Node's own networking stack — letting an attacker slip a private/loopback address past an allowlist that used `Address4`/`Address6` for validation. This PR pins and upgrades `ip-address` from `10.1.0` to `10.3.1` in both `package.json` (via `overrides`) and `package-lock.json`, eliminating the parser divergence across the whole dep