mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-12 05:32:36 +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:
@@ -207,7 +207,11 @@ func handleWebSocketOrDefault(c fiber.Ctx) error {
|
||||
if IsWebSocketRequest(c) {
|
||||
wsp := GetWebSocketProxy()
|
||||
if wsp != nil {
|
||||
extractedUserID, extractedRoleName := extractUserInfo(c)
|
||||
extractedUserID, extractedRoleName, authErr := extractUserInfo(c)
|
||||
if authErr != nil {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired authorization token")
|
||||
}
|
||||
|
||||
if checkIfUserIsBanned(c, extractedUserID) {
|
||||
return c.Status(fiber.StatusForbidden).SendString("User is banned")
|
||||
@@ -378,7 +382,11 @@ func processGraphQLRequest(c fiber.Ctx) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// Extract user information and check permissions
|
||||
extractedUserID, extractedRoleName := extractUserInfo(c)
|
||||
extractedUserID, extractedRoleName, authErr := extractUserInfo(c)
|
||||
if authErr != nil {
|
||||
cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil)
|
||||
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired authorization token")
|
||||
}
|
||||
|
||||
// Check if user is banned
|
||||
if checkIfUserIsBanned(c, extractedUserID) {
|
||||
@@ -424,15 +432,31 @@ func processGraphQLRequest(c fiber.Ctx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractUserInfo extracts user ID and role from request headers
|
||||
func extractUserInfo(c fiber.Ctx) (string, string) {
|
||||
extractedUserID := "-"
|
||||
extractedRoleName := "-"
|
||||
// extractUserInfo extracts the user ID and role from request headers.
|
||||
//
|
||||
// On a JWT signature verification failure (cfg.Client.JWTVerifier set and
|
||||
// the Authorization header carries an invalid/expired/forged token), it
|
||||
// returns ("-", "-", err) without applying the RoleFromHeader override, so
|
||||
// callers can 401 the request instead of proxying it under a wrong or
|
||||
// forged identity (GHSA-9gqw-h2rw-44wv). A missing Authorization header is
|
||||
// never an error; the request stays anonymous, exactly like before
|
||||
// verification existed.
|
||||
func extractUserInfo(c fiber.Ctx) (string, string, error) {
|
||||
extractedUserID := defaultValue
|
||||
extractedRoleName := defaultValue
|
||||
|
||||
// Extract from JWT if available
|
||||
// Extract from JWT if available. The claim-path check keeps the legacy
|
||||
// (verify-off) gate unchanged; when a verifier IS configured, any
|
||||
// Authorization header must still be checked even with no claim path
|
||||
// set, otherwise JWT_VERIFY_SIGNATURE=true alone would never call
|
||||
// Verify and the forged-token rejection below would never trigger.
|
||||
if authorization := c.Get("Authorization"); authorization != "" &&
|
||||
(len(cfg.Client.JWTUserClaimPath) > 0 || len(cfg.Client.JWTRoleClaimPath) > 0) {
|
||||
extractedUserID, extractedRoleName = extractClaimsFromJWTHeader(authorization)
|
||||
(len(cfg.Client.JWTUserClaimPath) > 0 || len(cfg.Client.JWTRoleClaimPath) > 0 || cfg.Client.JWTVerifier != nil) {
|
||||
usr, role, err := extractClaimsFromJWTHeader(authorization)
|
||||
if err != nil {
|
||||
return defaultValue, defaultValue, err
|
||||
}
|
||||
extractedUserID, extractedRoleName = usr, role
|
||||
}
|
||||
|
||||
// Override role from header if configured
|
||||
@@ -442,7 +466,7 @@ func extractUserInfo(c fiber.Ctx) (string, string) {
|
||||
}
|
||||
}
|
||||
|
||||
return extractedUserID, extractedRoleName
|
||||
return extractedUserID, extractedRoleName, nil
|
||||
}
|
||||
|
||||
// handleCaching manages the caching logic for GraphQL requests
|
||||
|
||||
Reference in New Issue
Block a user