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:
- Someone uploads a print job (or the OctoPrint instance is compromised, or its hostname is spoofed on the local network).
- The file's metadata contains a thumbnail reference — an arbitrary string.
- The application reads that metadata and calls the thumbnail helper.
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()withresponseType: '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
thumbnailUrlbefore the call is worthless withoutmaxRedirects: 0. An allowlisted host can still 302 you to169.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-dom7.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
thumbnailUrlvalue 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 andresponseType: '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-dom7.9.1 → 7.18.4 for CVE-2026-21884; the SSRF sink requires resolvingthumbnailUrlagainst the configured printer origin, rejecting non-HTTP schemes and internal addresses, and disabling redirects on theaxios.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.