mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-21 06:37:21 +00:00
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:
@@ -273,6 +273,22 @@ func parseConfig() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// JWT signature verification (GHSA-9gqw-h2rw-44wv): read here, next to the
|
||||
// claim-path variables, but the verifier itself is built further down
|
||||
// (after c.Logger exists) so a fatal misconfiguration can be logged
|
||||
// through the normal logger, matching the other fatal-config-error
|
||||
// patterns in this function. Off by default -- JWT_VERIFY_SIGNATURE=false
|
||||
// or unset keeps today's unverified claim decode (see
|
||||
// extractClaimsFromJWTHeader in details.go) byte-for-byte unchanged.
|
||||
jwtVerifySignature := getDetailsFromEnv("JWT_VERIFY_SIGNATURE", false)
|
||||
jwtSecret := getDetailsFromEnv("JWT_SECRET", "")
|
||||
jwtPublicKey := getDetailsFromEnv("JWT_PUBLIC_KEY", "")
|
||||
jwtJWKSURL := getDetailsFromEnv("JWT_JWKS_URL", "")
|
||||
jwtSigningMethods := getDetailsFromEnv("JWT_SIGNING_METHODS", "")
|
||||
jwtIssuer := getDetailsFromEnv("JWT_ISSUER", "")
|
||||
jwtAudience := getDetailsFromEnv("JWT_AUDIENCE", "")
|
||||
jwtLeewaySeconds := getDetailsFromEnv("JWT_LEEWAY_SECONDS", 0)
|
||||
|
||||
c.Client.RoleFromHeader = getDetailsFromEnv("ROLE_FROM_HEADER", "")
|
||||
c.Client.RoleRateLimit = getDetailsFromEnv("ROLE_RATE_LIMIT", false)
|
||||
// In-memory cache
|
||||
@@ -665,6 +681,42 @@ func parseConfig() {
|
||||
|
||||
// Note: RPS tracker is initialized in main() with context for graceful shutdown
|
||||
|
||||
// Initialize JWT signature verification (GHSA-9gqw-h2rw-44wv). The proxy
|
||||
// derives per-user cache keys from JWT claims (see details.go); those
|
||||
// claims were previously trusted without checking the token's signature,
|
||||
// so a forged token with a guessed claim could read another user's
|
||||
// cached response. A misconfigured key source here is fatal: the proxy
|
||||
// must never boot half-configured, trusting claims it cannot actually
|
||||
// verify.
|
||||
if jwtVerifySignature {
|
||||
verifier, err := newJWTVerifier(jwtVerifierConfig{
|
||||
Secret: jwtSecret,
|
||||
PublicKeyPEM: jwtPublicKey,
|
||||
JWKSURL: jwtJWKSURL,
|
||||
SigningMethods: jwtSigningMethods,
|
||||
Issuer: jwtIssuer,
|
||||
Audience: jwtAudience,
|
||||
LeewaySeconds: jwtLeewaySeconds,
|
||||
})
|
||||
if err != nil {
|
||||
cfg.Logger.Error(&libpack_logging.LogMessage{
|
||||
Message: "Failed to initialize JWT signature verification",
|
||||
Pairs: map[string]any{"error": err.Error()},
|
||||
})
|
||||
|
||||
if ifNotInTest() {
|
||||
fmt.Fprintln(os.Stderr, "❌ CRITICAL ERROR: JWT_VERIFY_SIGNATURE is enabled but misconfigured")
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
cfg.Client.JWTVerifier = verifier
|
||||
cfg.Logger.Info(&libpack_logging.LogMessage{
|
||||
Message: "JWT signature verification enabled",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Load rate limit configuration with improved error handling
|
||||
if err := loadRatelimitConfig(); err != nil {
|
||||
// Log the error with clear guidance
|
||||
|
||||
Reference in New Issue
Block a user