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

i18next-fs-backend 2.6.4 Prototype Pollution via Crafted Missing-Key

A critical prototype pollution vulnerability in i18next-fs-backend 2.6.4 allows attackers to modify Object.prototype through maliciously crafted translation key strings. The fix upgrades the package from 2.6.4 to 2.6.6, eliminating the unsafe key handling that permitted this attack vector.

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.

high

linkify-it 5.0.1 mailto: Link Parsing Causes DoS

linkify-it versions up to 5.0.1 can be forced into excessive processing time when autolinking a specially crafted mailto: link, allowing a remote attacker to degrade or stall the parsing thread. Upgrading to linkify-it 5.0.2 closes the issue; any application that runs linkify-it (directly or via markdown-it) against untrusted text should update immediately.

critical

Slim CLI Unverified Remote Fetch in Version Check

The Slim CLI's version check command fetched remote package metadata without integrity verification, enabling attackers to serve malicious responses through repository hijacking or man-in-the-middle attacks. The fix adds input validation, HTTP status checking, and response schema verification to ensure only legitimate version data is processed.

critical

Hybridauth Telegram OAuth Timing Attack in `authenticateCheckError()`

The Telegram OAuth provider in Hybridauth relied on `strcmp()` to validate HMAC-SHA256 signatures, exposing a timing side-channel that could allow attackers to forge authentication tokens character by character. The fix replaces this with PHP's timing-safe `hash_equals()` function, eliminating the information leak.