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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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 scheme — file://, 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

high

axios 1.18.1 CVE-2026-101898: HTTP/2 Ignores proxy and lookup

A dependency upgrade moved `axios` from 1.18.1 to 1.20.0 to pick up the fix for CVE-2026-101898, in which proxy and DNS resolution settings supplied through axios request configuration were not applied on the HTTP/2 request path. Applications that rely on `config.proxy`, `HTTP_PROXY`/`NO_PROXY` environment variables, or a custom `lookup` function as an SSRF or egress control could have those controls silently skipped for HTTP/2 requests.

critical

Cloudflare Worker Reverse Proxy SSRF via String Concatenation

A Cloudflare Worker reverse proxy built its upstream request URL by string-concatenating an environment variable with the incoming request's pathname and search string, letting a crafted path or query redirect requests to attacker-chosen hosts. The fix replaces concatenation with the `URL` object's `pathname` and `search` setters, which normalize input against the configured origin instead of blindly appending it.

high

fast-uri 3.1.2 IDN Host Bypass: CVE-2026-13676 Upgrade

A dependency tree in this repository resolved `fast-uri` 3.1.2, a version affected by CVE-2026-13676, in which improper Unicode hostname canonicalization lets a crafted URI parse to one host while the HTTP client connects to another. Because `fast-uri` is what `ajv` uses to parse and resolve URIs for `format: "uri"` validation and `$ref` resolution, any host allowlist built on its parsed output could be bypassed. The fix upgrades the resolved copy to `fast-uri` 4.1.4 and pins it with an npm `ove

high

boardcards.js parseArgs() SSRF: --board Flag Reaches Metadata IPs

The `parseArgs()` function in boardcards.js accepted a user-controlled `--board` command-line argument and passed it directly to fetch() calls without validating the URL scheme or hostname. An attacker could supply `http://169.254.169.254/latest/meta-data/` or other internal addresses to extract cloud credentials or scan internal networks.

high

fetch() with Automatic Redirects Leaks HTTPS Trust to api.tomys.top

The `withProxy()` fetch wrapper allowed automatic HTTP redirects from `api.tomys.top`, trusting the external API's domain integrity without validating final destinations. A compromised or malicious redirect response could send users to attacker-controlled phishing sites while maintaining HTTPS protocol and hostname checks that falsely appear secure.

high

Meta.get() Honored @uploadURL From Script Headers: SSRF Fix

A userscript manager's metadata parser accepted the `@uploadURL` directive from a script's own header block, so any installed or auto-updated script could silently redirect the options-page `uploadScript()` fetch to an attacker-chosen destination — including loopback, link-local metadata endpoints, and LAN addresses. The fix makes `Meta.get()` ignore `@uploadURL` when it appears in a script header, leaving the User Metadata field (validated in `Meta.getUserMeta()`) as the only way to set it, and