Back to Blog
high SEVERITY3 min read

fetch() with Automatic Redirects Leaks HTTPS Trust to api.tomys.top

The `withProxy()` fetch wrapper allowed automatic HTTP redirects from `api.tomys.top`, trusting the external API's domain integrity without validating final destinations. A compromised or malicious redirect response could send users to attacker-controlled phishing sites while maintaining HTTPS protocol and hostname checks that falsely appear secure.

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

Answer Summary

The `withProxy()` utility in model/utils.js used automatic redirects when fetching from `api.tomys.top`. An attacker achieving control of redirect responses from that external API could redirect users to phishing domains. The fix adds `redirect: 'manual'` to the fetch options, forcing explicit validation before following any redirect. CWE-601: URL Redirection to Untrusted Site.

Vulnerability at a Glance

cweCWE-601
fix`redirect: 'manual'` disables automatic following
riskPhishing via trusted-appearing redirect chain
languageJavaScript
root cause`redirect: 'follow'` (default) accepted destination URLs without application-level validation
vulnerabilityOpen Redirect / Unvalidated Redirect

Affected Versions

Affected unknown (first-party code)
Fixed in unknown (see PR commit)
Ecosystem not applicable (first-party code)
CVE / GHSA not assigned
CWE CWE-601: URL Redirection to Untrusted Site

The Vulnerability Explained

The withProxy() utility wraps native fetch() calls to route traffic through configurable proxies. When fetching random background images, it constructed options like this:

const fetchOptions = withProxy(
  { method: 'GET', signal: AbortSignal.timeout(5000) },
  pluginConfig,
  pluginConfig.proxy?.randomBackground,
  // ...
);

The problem: fetch() defaults to redirect: 'follow', meaning any 301/302/307 response is automatically followed without giving the application code a chance to inspect the destination. The existing validation checked that the original URL used HTTPS and matched the expected hostname—but this happens before the redirect, not after.

Here's the attack chain:
1. Application calls withProxy() to fetch from https://api.tomys.top/random-bg
2. Compromised API returns 302 Found with Location: https://attacker-phishing.com/malware
3. fetch() automatically follows to the attacker's site
4. The response returns to the application, which has no visibility into the redirect chain

The HTTPS and hostname checks provided false confidence. They validated the initial request, not where the client actually ended up.

The Fix

The change adds explicit redirect control to disable automatic following:

const fetchOptions = withProxy(
  { method: 'GET', signal: AbortSignal.timeout(5000), redirect: 'manual' },
  pluginConfig,
  pluginConfig.proxy?.randomBackground,
  // ...
);

With redirect: 'manual', fetch() returns the 3xx response instead of following it. The application can now:
- Inspect response.headers.get('location')
- Validate the destination against an allowlist
- Reject suspicious redirects before any subsequent request

This shifts security control from the external API's integrity back to the application. The signal: AbortSignal.timeout(5000) timeout remains, protecting against slow responses, but now the redirect chain is equally protected.

Key Takeaways

  • redirect: 'follow' (the default) is dangerous for external API calls: Never assume HTTPS on the original URL guarantees HTTPS on every hop in a redirect chain.

  • Proxy wrappers need redirect awareness: The withProxy() abstraction obscured fetch() behavior; security-critical options must be explicit, not inherited from defaults.

  • Hostname validation must cover final destinations: Checking api.tomys.top on request start misses attacker.com on response completion.

  • Manual redirects enable defense in depth: Even if api.tomys.top is later compromised, the application can reject unexpected destinations rather than blindly following them.

How Orbis AppSec Detected This

Source: The pluginConfig.proxy?.randomBackground configuration value controlling proxy routing for background image fetches.

Sink: The withProxy() utility's internal fetch() call, invoked with default redirect: 'follow' behavior.

Missing control: No validation of response.url after automatic redirect following; the application had no mechanism to inspect or reject redirect destinations from api.tomys.top.

CWE: CWE-601 — URL Redirection to Untrusted Site

Fix: Added redirect: 'manual' to fetch options, converting automatic redirects into inspectable responses that require explicit application-level validation before following.

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

This fix demonstrates how default fetch() behavior can undermine application security when calling external APIs. The withProxy() utility's partial HTTPS and hostname validation created a dangerous blind spot: it checked the front door but ignored where the redirect chain led. By switching to redirect: 'manual', the code regains control over the entire request lifecycle—essential when any external domain, even trusted ones like api.tomys.top, might return redirect responses.

Prevention and further reading

Frequently Asked Questions

Does the `withProxy()` utility still support proxying through api.tomys.top after the fix?

Yes, but fetch calls now receive `redirect: 'manual'`, requiring explicit handling of 3xx responses rather than automatically following them.

Why was hostname matching insufficient protection for the random background image fetch?

Hostname validation only checked the original URL, not redirect destinations. A compromised `api.tomys.top` could return a 302 to `attacker.com` while the validation logic saw only the initial HTTPS hostname match.

Which fetch option parameter changed from default behavior to explicit control?

The `redirect` option changed from implicit `'follow'` to explicit `'manual'` in the options object passed to `withProxy()`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #154

Related Articles

high

Meta.get() Honored @uploadURL From Script Headers: SSRF Fix

A userscript manager's metadata parser accepted the `@uploadURL` directive from a script's own header block, so any installed or auto-updated script could silently redirect the options-page `uploadScript()` fetch to an attacker-chosen destination — including loopback, link-local metadata endpoints, and LAN addresses. The fix makes `Meta.get()` ignore `@uploadURL` when it appears in a script header, leaving the User Metadata field (validated in `Meta.getUserMeta()`) as the only way to set it, and

high

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

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

Unbounded Map in createLoginRateLimiter Exhausts API Memory

The console API's `createLoginRateLimiter` and `createMutationRateLimiter` stored one `Map` entry per client key with no upper bound and no expiry sweep, so an attacker rotating source addresses or identifiers could grow those maps until the Node process hit an out-of-memory crash. The fix introduces a `maxTrackedKeys` option (default 5000), a `trackedEntryLimit` sanitizer, and an `evictOldestIfFull` helper that drops the oldest tracked key while protecting the shared global counter. The rate li