Back to Blog
high SEVERITY8 min read

Unvalidated thumbnailUrl Passed to axios.get(): SSRF Risk

A server-side image helper passes a `thumbnailUrl` value — sourced from 3D-print file metadata returned by an OctoPrint instance — straight into `axios.get()` with no scheme, host, or IP validation. Anyone who can write that metadata (a crafted G-code file, or a compromised/spoofed OctoPrint endpoint) turns the application into an HTTP proxy for internal networks and cloud metadata services. The accompanying pull request bumps `react-router-dom` from 7.9.1 to 7.18.4 to pick up the CVE-2026-21884

O
By Orbis AppSec
•Published September 24, 2026•Reviewed September 24, 2026

Answer Summary

The affected code is a first-party thumbnail image utility that calls `axios.get(thumbnailUrl)` server-side, using a URL taken from printer file metadata; the same repository was also flagged for `react-router-dom` 7.9.1 (CVE-2026-21884, fixed upstream in 7.12.0). An attacker who controls `thumbnailUrl` can make the server request `http://169.254.169.254/latest/meta-data/…`, reach internal-only HTTP services, or probe internal ports by timing the failures — and, because the fetched bytes are returned to the caller as image data, read the responses. The shipped pull request raises `react-router-dom` to 7.18.4; the SSRF requires a separate change: validate the scheme, resolve and reject private/link-local IPs, and set `maxRedirects: 0` before calling `axios.get()`. The finding class is CWE-918 (Server-Side Request Forgery).

Vulnerability at a Glance

cweCWE-918
fixDependency bump `react-router-dom` 7.9.1 → 7.18.4 for CVE-2026-21884; allowlist the thumbnail origin and disable redirects at the `axios.get()` call
riskServer-side fetch of attacker-chosen URLs — cloud metadata credential theft, internal service access, internal port scanning
languageTypeScript (Node.js)
root cause`thumbnailUrl` from printer file metadata is passed directly to `axios.get()` with no scheme, host, or IP checks and with default redirect following
vulnerabilityServer-Side Request Forgery (SSRF) via unvalidated thumbnail URL

Summary

A server-side image helper in this application fetches print-job thumbnails by handing a thumbnailUrl string directly to axios.get(). That string does not originate with the application — it comes from 3D-print file metadata exposed by an OctoPrint instance. No scheme check, no host allowlist, no IP-range check, and axios's default redirect following left in place. The result is a general-purpose HTTP fetch primitive controlled by whoever can write printer metadata.

The pull request that accompanies this finding raises react-router-dom from 7.9.1 to 7.18.4, which carries the upstream fix for CVE-2026-21884 (fixed in react-router 7.12.0). That is a separate, dependency-level issue. The SSRF sink is first-party code and needs its own change, described below.

Affected Versions

Affected not applicable (first-party code) — the thumbnail-fetch helper that calls axios.get(thumbnailUrl). The same scan also flagged react-router-dom 7.9.1 / react-router 7.9.1 for CVE-2026-21884.
Fixed in unknown for the first-party helper (no release boundary — see "The Fix"). For CVE-2026-21884: react-router 7.12.0; this repository moved to 7.18.4.
Ecosystem npm
CVE / GHSA CVE-2026-21884 (dependency finding); GHSA not assigned
CWE CWE-918: Server-Side Request Forgery (SSRF) — finding class; the advisory metadata lists CWE as unknown

The Vulnerability Explained

The helper's job is mundane: given a print job, resolve its preview image so the UI can render it. The call shape is this:

// thumbnailUrl arrives from OctoPrint file metadata
const response = await axios.get(thumbnailUrl, {
  responseType: 'arraybuffer',
});
// bytes are then converted and handed back to the caller

One line, one parameter, and the entire trust boundary crossed silently. thumbnailUrl is treated as if the application authored it. It did not.

Where the value actually comes from

OctoPrint surfaces per-file metadata through its API, and slicers embed thumbnail references inside the uploaded G-code and 3MF files themselves. The chain looks like:

  1. Someone uploads a print job (or the OctoPrint instance is compromised, or its hostname is spoofed on the local network).
  2. The file's metadata contains a thumbnail reference — an arbitrary string.
  3. The application reads that metadata and calls the thumbnail helper.
  4. axios.get() issues the request from the server, inside the server's network, with the server's egress identity.

Step 4 is the whole vulnerability. The attacker does not need network access to the internal targets; they only need to get a string into metadata that the server will dutifully dereference.

Concrete attacks against this exact call

Cloud metadata credential theft. If the service runs on a cloud instance with IMDSv1 reachable:

thumbnailUrl = http://169.254.169.254/latest/meta-data/iam/security-credentials/

Because responseType: 'arraybuffer' accepts any bytes and the helper returns those bytes to the caller as image data, the JSON credential blob flows back out of the service. An SSRF that returns the response body is not blind — it is a read primitive.

Internal service access. http://127.0.0.1:9200/_cat/indices, http://redis.internal:6379/, an admin panel bound to localhost that assumes loopback means trusted, an unauthenticated Prometheus or Actuator endpoint. Every one of these is a plain HTTP GET, which is exactly what this helper performs.

Port and host enumeration. Attackers do not need the body to learn something. ECONNREFUSED returns fast, a filtered port hangs until timeout, and an open HTTP port returns a status. Feeding a sequence of http://10.0.x.y:PORT/ values through the metadata field turns the thumbnail loader into a slow but reliable internal network scanner.

Redirect laundering. Even a naive hostname check is bypassable here, because axios follows up to five redirects by default. A URL on an approved host can respond 302 Location: http://169.254.169.254/…, and axios will follow it without re-consulting any validation the application performed on the original string.

Non-HTTP schemes. The absent scheme check is worth naming separately: nothing in the call rejects a scheme other than http/https, so scheme handling depends entirely on adapter behaviour rather than on an explicit decision made by this code.

Real-world impact

For a service that renders printer dashboards, the blast radius is not "someone sees a broken thumbnail." It is: cloud instance credentials disclosed, internal HTTP APIs reachable from the public internet by proxy, and an internal network map handed to an attacker whose only capability was uploading a G-code file.

The Fix

What the pull request actually changed

The shipped diff is a dependency upgrade — package.json and the lockfile — moving react-router-dom from the ^7.9.1 range to ^7.18.4, which pulls react-router 7.18.4 transitively:

-  "react-router-dom": "^7.9.1",
+  "react-router-dom": "^7.18.4",

CVE-2026-21884 is fixed upstream in react-router 7.12.0; 7.18.4 is comfortably past that boundary. The change was automated and is unverified against this repository's runtime behaviour, so treat it as a version-hygiene fix: it closes the advisory, and it does nothing to the SSRF.

What the SSRF sink needs

Because the vulnerable call is first-party code, there is no version to upgrade to. The fix is to make the trust boundary explicit at the axios.get() call site:

const u = new URL(thumbnailUrl, octoPrintBaseUrl);
if (u.protocol !== 'http:' && u.protocol !== 'https:') throw new Error('scheme');
if (u.origin !== new URL(octoPrintBaseUrl).origin) throw new Error('origin');
const { address } = await dns.lookup(u.hostname);
if (isPrivateOrLinkLocal(address)) throw new Error('internal address');

const response = await axios.get(u.toString(), {
  responseType: 'arraybuffer',
  maxRedirects: 0,          // defeat 302 -> 169.254.169.254
  timeout: 5000,
});

Each line closes a distinct hole in the original one-liner:

  • new URL(thumbnailUrl, octoPrintBaseUrl) — resolving against the configured OctoPrint base URL means relative thumbnail paths (the normal case) work, while absolute URLs become visible and checkable instead of implicitly honoured.
  • Scheme check — turns adapter-dependent scheme handling into an explicit two-value allowlist.
  • Origin check — the thumbnail should only ever come from the printer the application is already configured to talk to. That is an allowlist of exactly one origin, which is the strongest possible form.
  • DNS resolution plus private/link-local rejection — stops 169.254.169.254, 127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1, and attacker-controlled hostnames that resolve into those ranges.
  • maxRedirects: 0 — without this, every check above applies only to the first hop.
  • timeout — bounds the timing oracle and prevents a hung fetch from tying up a worker.

A practical hardening note: if the OctoPrint instance itself legitimately lives on a private address, a blanket private-IP block will break the feature. In that case the origin allowlist is the load-bearing control, maxRedirects: 0 is mandatory, and the DNS check should assert the resolved address equals the expected printer address rather than merely "not private."

Key Takeaways

  • A URL that arrives inside uploaded file metadata is attacker input. Thumbnail references embedded in G-code and 3MF files are authored by whoever uploaded the print job, not by your application.
  • axios.get() with responseType: 'arraybuffer' happily returns credential JSON. Because the helper hands those bytes back to the caller, this SSRF is a read primitive, not a blind one — the IMDS response leaves the network.
  • axios follows redirects by default, so validating thumbnailUrl before the call is worthless without maxRedirects: 0. An allowlisted host can still 302 you to 169.254.169.254.
  • When the outbound URL should only ever point at one configured service, resolve it relative to that service's base URL and compare origins. A one-entry allowlist beats any denylist of "bad" hosts.
  • The react-router-dom 7.9.1 → 7.18.4 bump closes CVE-2026-21884 and nothing else. Dependency upgrades and first-party sink hardening are separate pieces of work; shipping one does not discharge the other.

How Orbis AppSec Detected This

  • Source: the thumbnailUrl value read from 3D-print file metadata returned by the OctoPrint file API — controllable by anyone who can upload a print job or who controls/spoofs the OctoPrint endpoint.
  • Sink: axios.get() invoked server-side with that value as the request URL and responseType: 'arraybuffer', with axios's default redirect following in effect.
  • Missing control: no URL scheme allowlist, no origin comparison against the configured OctoPrint base URL, no DNS resolution with private/link-local IP rejection, and no maxRedirects: 0 — so the first hop and every subsequent hop were attacker-selectable.
  • CWE: CWE-918 — Server-Side Request Forgery (SSRF).
  • Fix: the pull request upgrades react-router-dom 7.9.1 → 7.18.4 for CVE-2026-21884; the SSRF sink requires resolving thumbnailUrl against the configured printer origin, rejecting non-HTTP schemes and internal addresses, and disabling redirects on the axios.get() 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

Two findings landed on the same repository and it is worth keeping them apart. One is a dependency advisory, CVE-2026-21884, resolved by moving react-router-dom past 7.12.0 — this project went to 7.18.4. The other is a design flaw in first-party code: a thumbnail loader that treats a string from uploaded printer metadata as a trustworthy URL and dereferences it server-side.

The second one is the dangerous one, because no upgrade will ever fix it. axios.get(thumbnailUrl) will keep fetching whatever it is given until the code decides, explicitly, which origins and address ranges are acceptable and refuses to follow redirects away from them. Any time a server fetches a URL that it did not construct itself, that decision has to be written down in code — at the call site, before the request goes out.

Prevention and further reading

Frequently Asked Questions

Does bumping `react-router-dom` from 7.9.1 to 7.18.4 fix the `thumbnailUrl` SSRF?

No. The upgrade only addresses CVE-2026-21884 in `react-router`; the server-side `axios.get(thumbnailUrl)` call is first-party code and still accepts any URL until you add a scheme and host check.

Is an origin allowlist on `thumbnailUrl` sufficient if the OctoPrint host is trusted?

Not on its own — axios follows up to five redirects by default, so an allowlisted host can 302 the fetch to `169.254.169.254` or `127.0.0.1`. Pass `maxRedirects: 0` and re-validate any location you choose to follow.

Why is printer file metadata considered untrusted input here?

Thumbnail references are embedded in uploaded G-code/3MF files and surfaced by the OctoPrint API, so whoever uploads a print job — or anyone who can spoof or compromise the OctoPrint instance — controls the string that reaches `axios.get()`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #228

Related Articles

high

esearch() SSRF: requests.get() Trusted Any Host in the URL

A citation format-conversion script used by an AI research skill built HTTP URLs from user-supplied PMIDs, DOIs, arXiv IDs, and free-text queries, then passed the resulting string straight to `requests.get()` with no check that it still pointed at an intended API host. The fix introduces an `ALLOWED_HOSTS` set containing the three real upstream APIs and an `_is_allowed_url()` helper that compares `urlparse(url).hostname` against it before the request is issued. This closes a CWE-918 server-side

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho

high

stream_media_file SSRF: src Parameter Reaches requests.get()

A media-download helper accepted a fully attacker-controlled URL from the `src` query parameter and passed it straight to `requests.get()`, turning the service into an open HTTP proxy for internal networks and cloud metadata endpoints. The fix introduces an `assert_safe_url()` guard that resolves the hostname with `getaddrinfo()` and rejects private, loopback, link-local, reserved, and multicast addresses before any request is issued. The guard is now called at the top of both `download_media_fi

high

ip-address 10.2.0 SSRF: Inconsistent Parsing Bypasses IP Checks

The `ip-address` npm package version 10.2.0 contains an inconsistent parsing vulnerability that allows attackers to bypass IP-based access controls. By representing IPv4 addresses in IPv4-mapped IPv6 notation, attackers can trick applications into allowing requests to blocked internal addresses. Upgrading to 10.3.1 resolves this through stricter address normalization.

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

JOSMFileHack TransformerFactory XXE: External DTD Processing Enabled

OSM2World's JOSMFileHack utility, which processed OpenStreetMap files generated by the JOSM editor, contained an insecure TransformerFactory configuration that permitted external DTD and stylesheet access. The vulnerability was resolved by completely removing the vulnerable code path rather than hardening it in place.