Back to Blog
critical SEVERITY9 min read

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

O
By Orbis AppSec
Published September 1, 2026Reviewed September 1, 2026

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in a Node.js maintenance script (`maintenance/getImages.js`). The `getImage()` function called `axios.get(url)` with a URL read directly from the elements database, with no host or protocol validation. An attacker who could write to the database could redirect the server to fetch from internal endpoints like the AWS metadata service at `169.254.169.254`. The fix parses the URL and enforces that only `https://upload.wikimedia.org` is an allowed destination before any HTTP request is made.

Vulnerability at a Glance

cweCWE-918
fixParse the URL with `new URL()` and enforce an allowlist requiring `https:` protocol and `upload.wikimedia.org` hostname before making any request
riskAttacker can redirect server-side HTTP requests to internal network resources or cloud metadata endpoints, exposing credentials and infrastructure details
languageJavaScript (Node.js)
root cause`axios.get(url)` called with a database-sourced URL at line 48 of `getImages.js` without any host or protocol validation
vulnerabilityServer-Side Request Forgery (SSRF)

How Server-Side Request Forgery Happens in Node.js Maintenance Scripts and How to Fix It

Summary

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 limits outbound requests exclusively to upload.wikimedia.org over HTTPS.


Introduction

The maintenance/getImages.js script has a straightforward job: look up image URLs from the elements database and download each image to the local filesystem. It's the kind of utility script that rarely gets a second look from a security perspective — it's not a public API endpoint, it doesn't accept user input directly, and it lives in a maintenance/ folder that implies infrequent, controlled use.

But that apparent innocuousness is exactly what makes it dangerous. The getImage() function, at line 48, does this:

const response = await axios.get( url, { responseType: 'arraybuffer' } );

The url variable comes from the elements database. If anyone — or anything — can write a malicious URL into that database, the server running this script will obediently fetch it. And "fetch it" means making an outbound HTTP request from your server's network context, with access to everything your server can reach: internal APIs, admin dashboards, and critically, cloud metadata services.

This is Server-Side Request Forgery (SSRF), and it's consistently ranked in the OWASP Top 10 (A10:2021) because it turns your own server into an unwitting proxy for attackers.


The Vulnerability Explained

What the Vulnerable Code Looks Like

Before the fix, the relevant section of getImage() looked like this:

async function getImage( el, url ) {

    console.log( 'proceed image for "' + el + '" ...' );

    const response = await axios.get( url, { responseType: 'arraybuffer' } );
    const filetype = url.split( '.' ).reverse()[0].toString().toLowerCase();
    const filepath = path + el + '.' + filetype;
    // ... write file ...
}

There is no validation between receiving url from the database and passing it to axios.get(). The function trusts the database completely.

Why This Is a Problem

axios.get() will happily request any URL it's given — including:

  • http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS EC2 metadata service)
  • http://localhost:8080/admin (local admin interfaces)
  • http://10.0.0.1/ (internal VPC resources)
  • http://169.254.169.254/computeMetadata/v1/ (GCP metadata service)
  • file:///etc/passwd (local filesystem, depending on the HTTP client)

These addresses are unreachable from the public internet but are fully accessible from within the server's own network context. That's the core of SSRF: the attacker doesn't need network access to your internal systems — they just need to convince your server to make the request for them.

The Concrete Attack Scenario

Here's how this plays out against a cloud-hosted deployment:

  1. An attacker gains write access to the elements database. This could be through a separate SQL injection vulnerability, a compromised admin account, or a misconfigured database permission.

  2. The attacker inserts a malicious URL into the image.url field of an element record:
    http://169.254.169.254/latest/meta-data/iam/security-credentials/my-ec2-role

  3. The maintenance script runs (perhaps on a schedule, perhaps triggered manually). getImage() is called with this URL.

  4. axios.get() fetches the metadata endpoint. The AWS metadata service responds with the EC2 instance's IAM role credentials:
    json { "AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "...", "Expiration": "2024-..." }

  5. The response is written to disk as an "image" file. The attacker can now retrieve it, or the credentials may appear in logs.

  6. With valid IAM credentials, the attacker can access S3 buckets, RDS databases, Lambda functions — whatever the EC2 role permits.

This is not a theoretical attack. SSRF against cloud metadata services is one of the most well-documented real-world attack chains, responsible for several high-profile data breaches.


The Fix

What Changed

The fix adds URL validation at the top of getImage(), immediately after the log statement and before any network call is made:

async function getImage( el, url ) {

    console.log( 'proceed image for "' + el + '" ...' );

    const allowedHost = 'upload.wikimedia.org';
    const parsed = new URL( url );

    if( parsed.protocol !== 'https:' || parsed.hostname !== allowedHost ) {

        throw new Error( 'refused to fetch image for "' + el + '" from untrusted host "' + parsed.hostname + '"' );

    }

    const response = await axios.get( url, { responseType: 'arraybuffer' } );
    // ...
}

Before vs. After

Before (vulnerable):

// Line 48 — no validation, url comes directly from database
const response = await axios.get( url, { responseType: 'arraybuffer' } );

After (secure):

const allowedHost = 'upload.wikimedia.org';
const parsed = new URL( url );

if( parsed.protocol !== 'https:' || parsed.hostname !== allowedHost ) {
    throw new Error( 'refused to fetch image for "' + el + '" from untrusted host "' + parsed.hostname + '"' );
}

const response = await axios.get( url, { responseType: 'arraybuffer' } );

Why This Fix Works

1. new URL() provides reliable parsing. Rather than using string operations (like url.startsWith('https://'), which can be fooled by https://evil.com/https://upload.wikimedia.org), the fix uses the built-in URL constructor to parse the URL into its components. parsed.hostname gives you the actual host, immune to path-based tricks.

2. The allowlist is strict and explicit. Only upload.wikimedia.org is permitted. Not *.wikimedia.org, not wikimedia.org — exactly upload.wikimedia.org. This means subdomain takeover attacks on adjacent Wikimedia subdomains can't be leveraged to bypass the check.

3. HTTPS is enforced. The protocol check parsed.protocol !== 'https:' ensures that even if someone injects a URL pointing to the allowed host over HTTP, it will be rejected. This prevents downgrade attacks and ensures traffic is encrypted.

4. The error message is informative but safe. The thrown error includes the rejected hostname (parsed.hostname), which helps with debugging without leaking sensitive information. The error causes the script to skip the malicious entry rather than silently failing or writing unexpected data to disk.

5. The fix is non-breaking for valid inputs. All legitimate image URLs in this codebase point to Wikimedia Commons (hosted at upload.wikimedia.org). The allowlist is scoped to exactly the valid use case, so no legitimate functionality is affected.


Prevention & Best Practices

1. Always Validate URLs Before Making Outbound Requests

Whenever your code fetches a URL that comes from an external source — a database, an API response, a user input, a configuration file — treat it as untrusted. Parse it and validate it before use.

// Safe pattern: parse first, validate second, fetch third
const parsed = new URL( url );
if ( !ALLOWED_HOSTS.has( parsed.hostname ) || parsed.protocol !== 'https:' ) {
    throw new Error( `Blocked request to untrusted host: ${parsed.hostname}` );
}
const response = await axios.get( url );

2. Prefer Allowlists Over Blocklists

Blocklists (e.g., "reject requests to 169.254.169.254") are fragile. Attackers can bypass them with:
- Alternative IP representations: 0xa9fea9fe, 169.254.169.254 in octal
- DNS rebinding: a domain that resolves to a public IP during validation but a private IP during the actual request
- HTTP redirects: your server fetches a "safe" URL that redirects to a private one

An allowlist that says "only upload.wikimedia.org over HTTPS" eliminates all of these bypass techniques.

3. Apply SSRF Protections at the Network Level Too

Defense in depth: even if application-level validation is bypassed, network-level controls can limit the damage:
- Use egress firewall rules to block outbound requests to RFC 1918 private IP ranges
- On AWS, use IMDSv2 (requiring a PUT request before GET) to make metadata service SSRF harder
- Run maintenance scripts in isolated network environments without access to production infrastructure

4. Audit Database-Sourced URLs Regularly

Any field in your database that stores a URL and is later fetched by server-side code is a potential SSRF vector. Audit these patterns in code review and in static analysis.

5. Use Static Analysis to Catch SSRF

Tools like Semgrep can be configured to flag patterns where data flows from a database query to an HTTP client call without an intervening validation step. This is exactly the pattern Orbis AppSec detected in this case.

Relevant standards:
- OWASP SSRF Prevention Cheat Sheet
- CWE-918: Server-Side Request Forgery
- OWASP Top 10 A10:2021 – Server-Side Request Forgery


Key Takeaways

  • Database fields storing URLs are SSRF vectors. The image.url field in the elements database fed directly into axios.get() — any field like this needs validation before use in an HTTP client call.

  • new URL() is the right tool for URL parsing in Node.js. String matching on URLs is unreliable and bypassable; the built-in URL constructor gives you a structured, trustworthy breakdown of each component.

  • Maintenance scripts carry real production risk. getImages.js lives in a maintenance/ folder, but it runs with production database access and production network permissions. "Maintenance" doesn't mean "low security."

  • A one-line axios.get(url) can expose your entire cloud infrastructure if url is attacker-controlled and the script runs on a cloud VM with an IAM role attached.

  • Allowlists beat blocklists for SSRF. The fix doesn't try to enumerate bad hosts — it specifies exactly one good host (upload.wikimedia.org) and rejects everything else.


How Orbis AppSec Detected This

  • Source: The url parameter in getImage( el, url ) is populated from the elements database — an external, potentially attacker-controlled data store.
  • Sink: axios.get( url, { responseType: 'arraybuffer' } ) at line 48 of maintenance/getImages.js — an outbound HTTP request made with the unvalidated URL.
  • Missing control: No hostname validation, protocol check, or allowlist was applied between reading url from the database and passing it to axios.get().
  • CWE: CWE-918 — Server-Side Request Forgery (SSRF).
  • Fix: Introduced new URL() parsing and an explicit allowlist check restricting requests to https://upload.wikimedia.org before any HTTP call is made.

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

The getImages.js SSRF vulnerability is a textbook example of how a seemingly innocuous utility script can become a critical security risk. The script's job — downloading images from URLs stored in a database — is completely legitimate. But the absence of a single validation step transformed it into a potential gateway to cloud credential theft and internal network reconnaissance.

The fix is elegant in its simplicity: parse the URL, check the hostname and protocol against an explicit allowlist, and throw an error if anything doesn't match. Six lines of code close a critical attack vector. This is the kind of defense that's easy to add during development and very hard to add after a breach.

For developers writing similar scripts — anything that reads a URL from a database or external source and fetches it — the lesson is clear: validate before you fetch, use allowlists over blocklists, and treat every database-sourced value as potentially attacker-controlled.


References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker tricks a server into making HTTP requests to unintended destinations — such as internal services, cloud metadata endpoints, or localhost — by supplying a malicious URL that the server fetches on their behalf.

How do you prevent SSRF in Node.js?

Validate and restrict outbound URLs before making any HTTP request. Use an explicit allowlist of trusted hostnames and protocols, parse URLs with `new URL()` to extract the hostname reliably, and reject anything that doesn't match.

What CWE is Server-Side Request Forgery?

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

Is blocking private IP ranges enough to prevent SSRF?

No. IP blocklists can be bypassed using DNS rebinding, redirects, IPv6 addresses, or encoded forms of restricted IPs. A hostname allowlist — like the one added to `getImages.js` — is more robust because it restricts requests to known-good destinations rather than trying to enumerate all bad ones.

Can static analysis detect SSRF?

Yes. Static analysis tools like Semgrep can trace tainted data from untrusted sources (such as database reads) to HTTP client sinks like `axios.get()`. Orbis AppSec's multi-agent AI scanner detected exactly this pattern in `getImages.js`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #145

Related Articles

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

high

How Octal IP Address Parsing Leads to SSRF in Node.js and How to Fix It

CVE-2026-69192 reveals a critical inconsistency in the `ip-address` library where Address4 decodes leading-zero octets as decimal while DNS resolvers interpret them as octal, creating a dangerous parsing divergence. This mismatch allows attackers to bypass IP-based access controls and perform Server-Side Request Forgery (SSRF) attacks. The fix upgrades `ip-address` from 9.0.5 to 10.3.1, aligning parsing behavior with standard resolver implementations.

critical

How Server-Side Request Forgery (SSRF) happens in Node.js fetch wrappers and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in the `recon.mjs` script, where a fetch wrapper accepted arbitrary URLs without validation. This allowed attackers to access internal infrastructure and cloud metadata services. The fix implements comprehensive URL validation that blocks internal IP ranges, loopback addresses, and dangerous protocols before any network request is made.

high

How IP Address Parsing Inconsistencies Cause SSRF and Trust-Boundary Bypass in Node.js Applications

The `ip-address` library version 10.2.0 contained a critical parsing inconsistency where the `Address4` decoder interpreted leading-zero octets as decimal numbers, while most DNS resolvers and network systems interpreted them as octal. This mismatch allowed attackers to bypass IP-based access controls and SSRF filters. Upgrading to version 10.3.1 fixes this vulnerability by aligning the library's parsing behavior with standard resolver behavior.

critical

How Server-Side Request Forgery (SSRF) Happens in Node.js fetch Tools and How to Fix It

A critical Server-Side Request Forgery (SSRF) vulnerability in `plugins/tools/fetch.js` allowed attackers to access internal resources and cloud metadata endpoints by passing arbitrary URLs to the fetch command. The fix adds hostname resolution and private IP range validation before executing any HTTP requests, preventing attackers from targeting internal infrastructure.

critical

How hardcoded API credentials in client-side JavaScript happens in userscripts and how to fix it

The jhs-enhance.user.js userscript contained a hardcoded Imgur API Client-ID embedded directly in client-side JavaScript code, exposing it to anyone who installed or viewed the script source. This critical vulnerability allowed unauthorized users to extract and abuse the API credentials for unlimited image uploads. The fix replaced the hardcoded credential with a user-prompt mechanism that requires each user to provide their own Imgur Client-ID.