Safeguard
Application Security

Designing a secure Node.js API gateway: auth, rate limits, validation, and signing

CVE-2020-15084 let attackers forge JWTs against express-jwt because one algorithm check was missing — a case study in why gateways need four defense layers, not one.

Safeguard Research Team
Research
6 min read

In September 2020, the GitHub Advisory Database published GHSA-6g6m-m6h5-w9gf: every version of express-jwt up to 5.3.3 failed to enforce a required algorithms allow-list, and when paired with jwks-rsa, that gap let an attacker manipulate a token's alg header to bypass signature verification entirely. The fix landed in version 6.0.0, but the lesson generalizes past one package — a gateway that trusts a token's own header to decide how it should be checked has already lost. express-jwt still pulls roughly 640,000 weekly npm downloads today, yet its release cadence has slowed to a crawl, which is its own quiet warning about auditing the maintenance status of whatever sits in your authentication path. This matters because in a Node.js API gateway, authentication is only one of four independent layers a request has to clear — alongside rate limiting, input validation, and, increasingly, request signing — and treating any one of them as sufficient on its own is how gateways fail. OWASP's 2023 API Security Top 10 restructured its own list around this exact insight, renaming its old "lack of rate limiting" category to something broader. This piece walks through how to build each layer correctly, and why they need to stay architecturally separate.

Why does authentication design determine whether a gateway is a perimeter or a hole?

Authentication fails at the gateway most often not because cryptography is weak, but because verification logic trusts input it shouldn't. The express-jwt case is the clean example: without a hardcoded, server-side algorithms option, a library has to infer from the token's own alg header whether to verify with a public key (RS256) or a shared secret (HS256) — and an attacker who knows a service's RSA public key can craft an HS256 token signed with that public key as the HMAC secret, which a permissive verifier accepts. The fix isn't clever key management; it's an explicit allow-list, set once in gateway config, that the verifier can't be talked out of. The broader principle: authentication middleware should validate issuer, audience, expiry, and algorithm as four independent checks, and none of the four should be inferable from attacker-controlled fields in the request itself.

What does OWASP's API Security Top 10 say a gateway actually has to defend?

OWASP's 2023 edition keeps Broken Object Level Authorization at API1 — it has held that rank since the list's 2019 debut — because verifying who a caller is (authentication) says nothing about what that caller is allowed to touch (authorization). A gateway that authenticates every request but delegates object-level checks to downstream services is only half built. Broken Authentication sits at API2, covering credential stuffing, weak token issuance, and exactly the kind of algorithm-confusion bug express-jwt shipped. Notably, OWASP renamed its old "Lack of Resources & Rate Limiting" entry to Unrestricted Resource Consumption (API4) in the 2023 revision, reflecting that CPU, memory, and per-operation costs (think GraphQL query depth or expensive file conversions) are resource-exhaustion vectors that raw request-per-second limits don't fully capture. A gateway's rate-limiting layer needs to account for cost, not just count.

How should rate limiting actually work once you scale past one instance?

express-rate-limit remains the standard middleware for this layer in Express-based gateways — it's a mature, widely deployed package with an active release cadence (v8.5.2 at the time of writing) — but its default in-memory store is a trap in any horizontally scaled deployment. If a gateway runs behind a load balancer across three instances, an in-memory counter tracks only the requests that instance happened to receive, so a client can effectively triple its allowed rate by getting distributed across nodes. Correct behavior requires a shared store — Redis is the conventional choice — so every instance decrements the same counter for the same client key. Rate-limit keys also need care: keying purely on source IP breaks for clients behind NAT or corporate proxies, so authenticated gateways should key on the caller's token subject or API key wherever one is available, falling back to IP only for unauthenticated routes.

Why does input validation need to be its own layer, separate from authentication?

A caller can be perfectly authenticated and still send a payload designed to break something downstream — an oversized array that exhausts memory during JSON parsing, a string field containing SQL or NoSQL operator injection, or a numeric field carrying a value the business logic never expected. Authentication answers "is this caller who they claim to be," and authorization answers "may they act on this resource" — neither one inspects the shape or content of the request body. Schema validation (JSON Schema, or Node validators such as Zod or Ajv) belongs at the gateway edge specifically so malformed or hostile payloads are rejected before they reach business logic, database drivers, or any internal service that might parse them more permissively. This also gives a gateway a place to enforce size limits on request bodies before they're fully buffered, which is itself a resource-consumption control in the spirit of OWASP's API4.

What does request signing add that TLS alone doesn't?

TLS protects a request in transit between the client and whatever terminates the connection — but in most real deployments, that's a load balancer or CDN edge sitting in front of the gateway, not the gateway itself, so TLS says nothing about whether the request was tampered with or replayed after termination. Request signing — computing an HMAC or asymmetric signature over the method, path, timestamp, and body, in the style of AWS SigV4 — lets the gateway verify that a request's contents haven't changed since the client signed them, and a timestamp-plus-nonce check closes the replay window even if a captured request is retried later. This is conceptually the same trust problem Safeguard's own attestation-signing model solves for software artifacts: a Sigstore-based signature plus a Rekor transparency-log entry lets a verifier confirm an artifact hasn't been altered since a specific, provable point in time, independent of the channel it traveled over. Applied to live API traffic, the same principle — sign at the source, verify independently of transport — is what closes the gap TLS termination leaves open.

How Safeguard helps

Safeguard doesn't ship a Node.js API gateway product, but the architecture question underneath this post — how to add an enforcement layer to live traffic without that layer becoming a new single point of failure — is exactly what Safeguard's AI Gateway solves for LLM traffic today. Its guardrails run in monitor mode by default, evaluating every prompt, response, and tool call and emitting a telemetry event without altering traffic, so a team can baseline what an enforcement policy would actually do before flipping it on for real requests. Enforce mode is gated behind an explicit per-tenant flag once that baseline looks right. And the gateway is fail-open by default with a capped per-request evaluation budget: a timeout or internal error passes the request through with an alert raised, rather than taking the protected application down. That monitor-first, fail-open rollout discipline is the same posture worth building into any authentication, rate-limiting, validation, or signing layer you add to a Node.js gateway — ship it observing, prove it's correct, then let it block.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.