Back to Blog
critical SEVERITY5 min read

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published October 2, 2026•Reviewed October 2, 2026

Answer Summary

The affected API is the first-party `/api/uploads/presign` route in the upload service, not a published package. An attacker could send unbounded concurrent requests to this route to exhaust storage-provider API quotas, database connections, or server resources — a denial-of-service condition. The fix adds an `express-rate-limit` middleware instance (`presignRateLimiter`, 30 requests per 60-second window) in front of the route handler; no package version applies since this is first-party code. The CWE classification is unknown/unassigned for this finding.

Vulnerability at a Glance

cweunknown
fixAdded an `express-rate-limit` instance limiting the route to 30 requests per minute
riskUnauthenticated or authenticated clients can flood a storage-provider-backed endpoint, exhausting quotas and connections
languageJavaScript (Node.js / Express)
root causeRoute handler for `/api/uploads/presign` had no request-throttling middleware
vulnerabilityMissing rate limiting leading to resource exhaustion (DoS)

Introduction

The upload service exposes a route, /api/uploads/presign, whose entire job is to mint a presigned URL by calling out to a storage provider on behalf of the caller. That's an expensive operation by design — it talks to an external API, touches session state, and in most deployments counts against a provider quota. Despite that cost, the handler was registered with nothing standing between the public internet and the storage backend:

app.post("/api/uploads/presign", async (req, res) => {

No throttling, no concurrency cap, no per-client accounting. Any client that could reach this endpoint could call it as fast as the network would allow, and each call would do real work against a real external service. This matters for anyone building endpoints that wrap a paid or quota-limited API: the cost of a single malicious client scales directly with how cheap it is for them to send a request versus how expensive it is for your server to answer it.

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code)
Ecosystem npm
CVE / GHSA not assigned
CWE unknown

The Vulnerability Explained

The route handler for /api/uploads/presign ran authenticateSession(req, res) and then proceeded straight to generating a presigned upload URL — there was no middleware checking how many times a given IP, session, or client had already hit this path. The relevant exploitation scenario called out in the finding is direct: an attacker with network access sends sustained, high-volume POST requests to /api/uploads/presign.

Each of those requests, even if it fails authentication later in the handler, still consumes a request-handling thread, hits the event loop, and — for requests that do carry a valid session — triggers a real call out to the storage provider to generate a presigned URL. At volume, this does three kinds of damage simultaneously:

  • It burns through the storage provider's request quota, which is often billed or rate-capped by the provider itself, turning an application-level flood into a billing or availability incident at the infrastructure layer.
  • It exhausts the application server's own capacity (connection pool, event loop time) that legitimate upload traffic needs.
  • It gives an attacker a cheap, repeatable denial-of-service lever that requires no privilege beyond network access to the endpoint — no exploit of application logic needed, just volume.

The regression test added alongside the fix demonstrates exactly this: it fires 100 concurrent POST /api/uploads/presign requests and asserts that at some point the server starts returning 429 rather than processing every single one.

The Fix

The fix introduces a dedicated express-rate-limit instance, presignRateLimiter, scoped to a 60-second window with a cap of 30 requests, and wires it into the route as middleware:

const presignRateLimiter = rateLimit({
  windowMs: 60 * 1000,
  limit: 30,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: "Too many upload requests. Please slow down." },
});
app.post("/api/uploads/presign", presignRateLimiter, async (req, res) => {

This is a minimal, surgical change: the route's existing logic — authenticateSession, the storage-provider call, the response shape — is untouched. The only new behavior is that presignRateLimiter runs first and short-circuits with a 429 and the { error: "Too many upload requests. Please slow down." } payload once a client exceeds 30 requests inside the rolling 60-second window. standardHeaders: true also means clients get standard RateLimit-* response headers so well-behaved integrations can back off before hitting the cap, while legacyHeaders: false avoids sending the older X-RateLimit-* headers alongside them.

Because the limiter sits in front of authenticateSession, it protects the storage-provider call path regardless of whether the flood comes from authenticated or unauthenticated traffic — the expensive work never starts once the cap is hit.

Key Takeaways

  • A route that calls out to a third-party storage API is not the same cost profile as a route that reads from local memory — treat presign/upload endpoints as high-value rate-limiting targets first.
  • express-rate-limit's limit option (not the older max name) with windowMs: 60 * 1000 gives a simple per-minute budget; 30/min was chosen to allow normal upload bursts while blocking sustained floods.
  • Authentication is not a substitute for rate limiting — authenticateSession still ran on every request before the fix, but checking a session doesn't stop an attacker from sending the request in the first place.
  • Put the rate limiter in the middleware chain ahead of the expensive call, not inside the handler body, so rejected requests never reach the storage provider.
  • A load test that fires 100 concurrent requests at an endpoint and checks for 429 responses is a cheap, durable regression guard against this exact class of regression.

How Orbis AppSec Detected This

  • Source: unauthenticated or authenticated HTTP POST requests to /api/uploads/presign
  • Sink: the storage-provider presign call reachable through the route handler, invoked on every incoming request
  • Missing control: no rate-limiting middleware in the request pipeline to cap requests per client over time
  • CWE: unknown (not assigned for this finding)
  • Fix: an express-rate-limit instance (presignRateLimiter, 30 requests per 60-second window) was added as middleware in front of the route handler

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

An endpoint that wraps a third-party, quota-bound API is only as resilient as its weakest control — and in this case, the weakest control was simply missing. /api/uploads/presign had no mechanism to stop a client from calling it as fast as the network allowed, which meant a sustained flood could translate directly into exhausted storage-provider quotas and a degraded upload experience for everyone else. The fix doesn't rewrite the presign logic at all; it adds a 30-requests-per-minute express-rate-limit gate in front of it, so the expensive work only happens for clients operating within a reasonable budget.

Prevention and further reading

Frequently Asked Questions

Why is `/api/uploads/presign` singled out instead of every route in the uploads API?

It's the most resource-intensive handler in that file — each call generates a presigned URL by calling out to the storage provider, which is far costlier than a simple read, making it the highest-value target for flooding.

What limit does `presignRateLimiter` actually enforce?

It uses `express-rate-limit` with a `windowMs` of 60,000ms and a `limit` of 30, meaning a given client gets at most 30 presign requests per minute before receiving a 429 response.

Does adding `presignRateLimiter` change the authentication flow in `authenticateSession`?

No — the rate limiter runs as separate middleware before the handler executes; `authenticateSession(req, res)` is called exactly as before once a request passes the rate check.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #138

Related Articles

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.

critical

redmine_drawio View Hook Inlines Base64 Redmine API Keys

The Redmine drawio plugin's body-bottom view listener embedded the logged-in user's Redmine REST API key into client-side JavaScript on every wiki and issue page, "protected" only by Base64 encoding of the reversed string. Any script on the page — or anyone with browser developer tools, a cached copy of the HTML, or a proxy log — could decode it in one line and act as that user through the Redmine REST API. The fix removes the embedded credential from the rendered page entirely; the plugin's dia