Back to Blog
critical SEVERITY6 min read

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

A critical Server-Side Request Forgery vulnerability in `popup.js` allowed attackers to inject malicious URLs from scraped webpages directly into fetch() calls, potentially accessing internal network resources and AWS metadata endpoints. The fix adds URL validation to ensure only HTTP/HTTPS protocols are used and blocks requests to private IP ranges and localhost addresses.

O
By Orbis AppSec
Published July 25, 2026Reviewed July 25, 2026

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability in JavaScript's fetch() function (CWE-918) where user-controlled URLs from scraped webpages were passed directly to network requests without validation. The fix adds three critical checks: validating the URL protocol is HTTP/HTTPS, blocking requests to localhost, and rejecting private IP address ranges (10.x.x.x, 192.168.x.x, 172.16-31.x.x). This prevents attackers from using the application to probe internal networks or access cloud metadata endpoints.

Vulnerability at a Glance

cweCWE-918 (Server-Side Request Forgery)
fixAdd URL object validation to enforce HTTP/HTTPS protocols and block private IP ranges before calling fetch()
riskAttackers can force the application to make requests to internal network addresses, cloud metadata endpoints, or localhost services, potentially leaking sensitive data or enabling lateral movement
languageJavaScript (Node.js/Electron)
root causeURLs from scraped webpage content passed directly to fetch() without protocol or IP range validation
vulnerabilityServer-Side Request Forgery (SSRF) via unvalidated fetch() URLs

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

Introduction

The popup.js file in this Electron application handles image processing from scraped webpages—a seemingly benign operation that masked a critical Server-Side Request Forgery vulnerability. At line 646, multiple fetch() calls accepted image URLs directly from user-controlled sources without any validation. An attacker controlling scraped webpage content could inject URLs pointing to internal network addresses, AWS metadata endpoints (169.254.169.254), or localhost services, forcing the application to make requests it was never intended to make.

This vulnerability exemplifies a common misconception in web security: that the fetch() API is "safe by default" because it's a modern browser API. In reality, when fetch() is used in a server context (or in this case, an Electron application with network access), it becomes a powerful attack vector if URLs aren't properly validated.

The Vulnerability Explained

What Made This Code Vulnerable

The vulnerable code pattern in popup.js:646 looked like this:

func: async (imgUrl) => {
  try {
    const res = await fetch(imgUrl);
    const blob = await res.blob();
    return await new Promise(resolve => {
      // ... process blob
    });
  }
}

The problem is deceptively simple: imgUrl comes from scraped webpage content, and there's zero validation before it reaches fetch(). The developer likely assumed that only valid image URLs would be present, but this assumption breaks down when:

  1. Scraped content is untrusted: Webpages can be compromised, contain injected content, or be controlled by attackers
  2. URL parsing is lenient: JavaScript's URL constructor accepts many formats that developers don't expect
  3. No protocol filtering: The code would happily fetch from file://, gopher://, or other schemes if the JavaScript engine supports them
  4. No IP range filtering: Internal addresses like 10.0.0.1, 172.16.0.1, or 192.168.1.1 are treated the same as public URLs

Attack Scenarios Specific to This Code

Scenario 1: AWS Metadata Exfiltration
An attacker compromises a website that the application scrapes. They inject:

<img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/default" />

The application's fetch() call retrieves AWS credentials for the EC2 instance it's running on, and the attacker extracts them from error messages or response timing.

Scenario 2: Internal Service Exploitation
The attacker injects:

<img src="http://localhost:5000/admin/reset-password?token=admin" />

If the application is running alongside a local admin service, this triggers unintended administrative actions.

Scenario 3: Network Reconnaissance
The attacker injects multiple URLs like:

<img src="http://192.168.1.1/" />
<img src="http://192.168.1.100/" />
<img src="http://10.0.0.5:8080/" />

By observing response times and error patterns, they map the internal network topology.

The Fix

The PR introduced three critical validation checks before the fetch() call:

func: async (imgUrl) => {
  try {
    const _u = new URL(imgUrl);
    if (!['http:', 'https:'].includes(_u.protocol)) return null;
    if (/^(localhost$|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(_u.hostname)) return null;
    const res = await fetch(imgUrl);
    const blob = await res.blob();
    return await new Promise(resolve => {
      // ... process blob
    });
  }
}

What Changed and Why

Line 1: URL Parsing

const _u = new URL(imgUrl);

This parses the URL into its components. If imgUrl is malformed, new URL() throws an exception, which is caught by the existing try-catch block. This prevents protocol confusion attacks.

Line 2: Protocol Whitelist

if (!['http:', 'https:'].includes(_u.protocol)) return null;

This explicitly allows only HTTP and HTTPS. Without this check, an attacker could use:
- file:// to access local files
- ftp:// to connect to FTP servers
- gopher:// or other schemes depending on the runtime environment

By returning null instead of throwing, the code gracefully handles invalid URLs without crashing the image processing pipeline.

Line 3: Private IP Range Blocking

if (/^(localhost$|127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(_u.hostname)) return null;

This regex blocks:
- localhost (exact match)
- 127.x.x.x (loopback addresses)
- 10.x.x.x (private Class A)
- 192.168.x.x (private Class C)
- 172.16-31.x.x (private Class B)

These ranges cover all RFC 1918 private IP addresses plus loopback. The regex 172\.(1[6-9]|2\d|3[01]) specifically matches 172.16 through 172.31 using digit ranges.

Why This Approach is Correct

Rather than maintaining a whitelist of "safe" domains (which would be fragile), the fix uses a blacklist of dangerous patterns. This is appropriate here because:

  1. Simplicity: The application legitimately needs to fetch from arbitrary public image URLs
  2. Maintenance: Whitelisting would require knowing all valid image sources in advance
  3. Completeness: The private IP ranges are well-defined by RFC standards and unlikely to change

Prevention & Best Practices

For This Specific Code Pattern

  1. Always validate URLs before network calls: Treat URLs as user input, even if they come from scraped content
  2. Use the URL constructor: JavaScript's new URL() is safer than string manipulation and provides structured access to components
  3. Implement protocol whitelisting: Explicitly allow only HTTP/HTTPS for general web requests
  4. Block private IP ranges: Use the RFC 1918 ranges plus loopback and link-local addresses

Broader Security Recommendations

  • Content Security Policy (CSP): In browser contexts, CSP can restrict fetch destinations
  • Network segmentation: Isolate the application from internal networks when possible
  • Monitoring: Log all external URLs being fetched; unusual patterns may indicate exploitation attempts
  • Input validation at the source: Validate scraped URLs when they're extracted, not just when they're used

Detection Tools

  • Semgrep: Use the rule javascript.lang.security.audit.unvalidated-url-fetch to find similar patterns
  • Static analysis: ESLint plugins can flag fetch() calls with non-literal URLs
  • Dynamic analysis: Monitor network connections made by the application; unexpected internal requests are suspicious

Key Takeaways

  • URL validation in popup.js line 646 was the critical missing step: The code assumed all scraped URLs were safe, but attackers could inject internal addresses
  • The regex pattern blocks all RFC 1918 private ranges: This single check prevents most SSRF attacks without requiring a whitelist
  • Protocol whitelisting prevents scheme confusion attacks: Blocking file:// and gopher:// closes alternative attack vectors
  • The fix preserves legitimate functionality: Public image URLs still work; only internal/private addresses are blocked
  • SSRF is often overlooked in JavaScript: Many developers don't realize that fetch() can be weaponized to attack internal infrastructure

How Orbis AppSec Detected This

Component Details
Source Image URLs from scraped webpage content passed to the image processing function in popup.js
Sink fetch(imgUrl) call at line 646 in popup.js without URL validation
Missing Control No protocol validation, no private IP range checking, no URL format validation
CWE CWE-918: Server-Side Request Forgery (SSRF)
Fix Added URL object instantiation, protocol whitelist check (['http:', 'https:']), and regex-based private IP range blocking before the fetch() call

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

Server-Side Request Forgery vulnerabilities in JavaScript are particularly insidious because the fetch() API appears safe and modern, leading developers to trust it with user-controlled input. The fix in this PR demonstrates that security isn't about complex algorithms—it's about systematic validation at every trust boundary.

The three-line validation added to popup.js transforms an exploitable vulnerability into a hardened function. For developers working with scraped content, external APIs, or any untrusted URLs, this pattern is a template: parse the URL, whitelist the protocol, and block private IP ranges. These simple checks prevent entire classes of attacks.

As you review your own code, ask: "Where does my application make network requests based on external input?" The answer might reveal your own SSRF vulnerability waiting to be fixed.

References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker manipulates an application into making unintended HTTP requests to internal or restricted network addresses. In this case, attackers could inject malicious URLs in scraped webpage content that the application would fetch without validation.

How do you prevent SSRF in JavaScript?

Always validate URLs before passing them to fetch(): verify the protocol is HTTP/HTTPS, check against a whitelist if possible, and block requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), localhost, and link-local addresses (169.254.0.0/16).

What CWE is SSRF?

CWE-918 (Server-Side Request Forgery). Related CWEs include CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code) when eval() is involved, and CWE-434 (Unrestricted Upload of File with Dangerous Type).

Is checking the protocol enough to prevent SSRF?

No. While restricting to HTTP/HTTPS is important, attackers can still target internal services like AWS metadata (169.254.169.254), Kubernetes API servers, or localhost services. You must also validate the hostname/IP against private ranges.

Can static analysis detect SSRF?

Yes. Tools like Semgrep can detect patterns where user input flows directly into fetch(), request(), or similar functions. However, they may miss complex validation logic, so manual review is still recommended for security-critical code.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #62

Related Articles

critical

How arbitrary code execution via injected protobuf definition type fields happens in Node.js and how to fix it

A critical arbitrary code execution vulnerability (CVE-2026-41242) was discovered in protobufjs versions prior to 7.5.5 and 8.0.1, allowing attackers to inject malicious code through crafted protobuf definition type fields. The fix upgrades the dependency from the vulnerable version 6.11.4 to 7.5.5, which properly sanitizes type field inputs during protobuf parsing. This vulnerability is especially dangerous because protobufjs is widely used in Node.js applications for serialization, meaning a s

high

How command injection happens in Node.js child_process and how to fix it

A high-severity command injection vulnerability was discovered in `bootstrap.js` at line 34, where the `exec()` function was used to run shell commands constructed from a `folder` variable. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates the shell interpolation entirely, closing the door on command injection attacks that could have affected all downstream consumers of this Node.js library.

high

How unsafe pickle deserialization happens in NumPy's np.load() and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `tools/ardy-engine/retarget.py` where `np.load()` was called with `allow_pickle=True`, enabling attackers to embed malicious pickle payloads in `.npz` files. The fix was a single-character change—switching `allow_pickle=True` to `allow_pickle=False`—that eliminates the deserialization attack vector while preserving the file's legitimate array data loading functionality.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How pickle-based arbitrary code execution happens in PyTorch and how to fix it

A high-severity arbitrary code execution vulnerability was discovered in `scripts/export_joyvasa_audio.py` where `torch.load()` was called with `weights_only=False`, allowing any pickle-serialized Python object — including malicious code — to execute during checkpoint loading. The fix switches to `weights_only=True` and explicitly allowlists only the two non-standard classes the checkpoint actually requires: `argparse.Namespace` and `pathlib.PosixPath`. This closes a real code execution path tha

critical

How WebSocket protocol length header abuse happens in Node.js and how to fix it

A critical vulnerability (CVE-2026-54466) in websocket-driver versions prior to 0.7.5 allows attackers to corrupt WebSocket messages by manipulating protocol length headers. This can lead to data integrity issues, denial of service, or potentially arbitrary code execution in affected Node.js applications. The fix involves upgrading the websocket-driver dependency to version 0.7.5.