fix(auth): verify JWT signature to prevent cache-key forgery (GHSA-9gqw-h2rw-44wv)

The proxy derived per-user GraphQL cache keys from JWT claims without
verifying the token signature (CWE-345). A remote attacker could forge a
token that carried a victim's claim value and read the victim's cached
response.

This change adds opt-in signature verification behind JWT_VERIFY_SIGNATURE.
The flag defaults to false, so an upgrade without a config change keeps the
previous delegate-to-upstream behavior byte for byte.

When verification is on, the operator must set exactly one key source:
JWT_SECRET, JWT_PUBLIC_KEY, or JWT_JWKS_URL. The verifier pins the accepted
algorithms to the key family through WithValidMethods and a type-switched
keyfunc, and it refuses alg=none. This closes the algorithm-confusion class
of attack. A present but invalid token now returns 401. A missing
Authorization header stays anonymous.

Startup fails loudly when verification is on and the key source is absent or
unreadable, so the proxy can never boot half-configured.
This commit is contained in:
2026-08-17 13:51:00 +01:00
parent 99f0c844f0
commit 8258b3d99a
11 changed files with 881 additions and 36 deletions
+8 -1
View File
@@ -713,7 +713,14 @@ func performProxyRequest(c fiber.Ctx, proxyURL string) error {
// reached without going through handleCaching (direct callers).
cacheKey, _ := c.Locals("query_cache_hash").(string)
if cacheKey == "" {
userID, userRole := extractUserInfo(c)
// Error intentionally ignored: the primary rejection for an
// invalid/forged token already happened in processGraphQLRequest
// (it 401s before the request ever reaches here). On a verify
// failure extractUserInfo returns ("-", "-", err), so a forged
// token still only maps into the shared anonymous cache bucket
// below, never into a genuine, verified user's own bucket
// (GHSA-9gqw-h2rw-44wv).
userID, userRole, _ := extractUserInfo(c)
cacheKey = libpack_cache.CalculateHash(c, userID, userRole)
}