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'slimitoption (not the oldermaxname) withwindowMs: 60 * 1000gives 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 —
authenticateSessionstill 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
429responses is a cheap, durable regression guard against this exact class of regression.
How Orbis AppSec Detected This
- Source: unauthenticated or authenticated HTTP
POSTrequests 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-limitinstance (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.