From f93cbb6a7725c8a340da5f9eb4845ea71c49b671 Mon Sep 17 00:00:00 2001 From: zen-triager Date: Tue, 18 Aug 2026 12:33:04 +0000 Subject: [PATCH] fix issue --- .../helpers/tryDecodeAsJWT.go | 25 +++++++-- .../tryDecodeAsJWT_differential_test.go | 53 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 lib/request-processor/helpers/tryDecodeAsJWT_differential_test.go diff --git a/lib/request-processor/helpers/tryDecodeAsJWT.go b/lib/request-processor/helpers/tryDecodeAsJWT.go index 10f009406..e2d93179e 100644 --- a/lib/request-processor/helpers/tryDecodeAsJWT.go +++ b/lib/request-processor/helpers/tryDecodeAsJWT.go @@ -14,6 +14,24 @@ func removePadding(s string) string { return strings.TrimRight(strings.TrimLeft(s, "="), "=") } +// decodeBase64Segment decodes a JWT segment as leniently as common application +// decoders. Application-side JWT libraries frequently accept both URL-safe +// (-_) and standard (+/) base64 alphabets, so the firewall must too, otherwise +// claims can be smuggled past scanning by switching the alphabet. +func decodeBase64Segment(segment string) ([]byte, bool) { + segment = removePadding(segment) + encodings := []*base64.Encoding{ + base64.RawURLEncoding, + base64.RawStdEncoding, + } + for _, enc := range encodings { + if payload, err := enc.DecodeString(segment); err == nil { + return payload, true + } + } + return nil, false +} + func tryDecodeAsJWT(jwt string) JWTDecodeResult { if !strings.Contains(jwt, ".") { return JWTDecodeResult{JWT: false} @@ -22,15 +40,14 @@ func tryDecodeAsJWT(jwt string) JWTDecodeResult { if len(parts) != 3 { return JWTDecodeResult{JWT: false} } - //remove padding - payload, err := base64.RawURLEncoding.DecodeString(removePadding(parts[1])) - if err != nil { + payload, ok := decodeBase64Segment(parts[1]) + if !ok { return JWTDecodeResult{JWT: false} } var object interface{} - err = ParseJSON(payload, &object) + err := ParseJSON(payload, &object) if err != nil { return JWTDecodeResult{JWT: false} diff --git a/lib/request-processor/helpers/tryDecodeAsJWT_differential_test.go b/lib/request-processor/helpers/tryDecodeAsJWT_differential_test.go new file mode 100644 index 000000000..566f5e8c8 --- /dev/null +++ b/lib/request-processor/helpers/tryDecodeAsJWT_differential_test.go @@ -0,0 +1,53 @@ +package helpers + +import ( + "encoding/base64" + "encoding/json" + "strings" + "testing" +) + +// buildDifferentialTokens returns two JWTs carrying the identical claim, differing +// only in the base64 alphabet of the payload segment (url-safe vs standard). +func buildDifferentialTokens(t *testing.T) (urlSafe, standard, claim string) { + claim = "x' OR '1'='1" + var std string + for i := 1; i < 64; i++ { + obj := map[string]interface{}{"z": strings.Repeat("\u00ff", i), "name": claim} + payload, _ := json.Marshal(obj) + cand := strings.TrimRight(base64.StdEncoding.EncodeToString(payload), "=") + if strings.ContainsAny(cand, "+/") { + std = cand + break + } + } + if std == "" { + t.Fatal("could not build a payload with + or / in std base64") + } + url := strings.NewReplacer("+", "-", "/", "_").Replace(std) + hdr := strings.TrimRight(base64.URLEncoding.EncodeToString([]byte(`{"alg":"none"}`)), "=") + return hdr + "." + url + ".sig", hdr + "." + std + ".sig", claim +} + +// A JWT whose payload uses standard base64 (+/) must be decoded and its claims +// scanned identically to the url-safe form. +func TestTryDecodeAsJWTStandardBase64(t *testing.T) { + urlSafe, standard, claim := buildDifferentialTokens(t) + + if r := tryDecodeAsJWT(urlSafe); !r.JWT { + t.Fatalf("url-safe JWT should decode") + } + if r := tryDecodeAsJWT(standard); !r.JWT { + t.Fatalf("standard-base64 JWT should decode (differential bypass)") + } + + urlStrings := ExtractStringsFromUserInput(urlSafe, []PathPart{}, 0) + stdStrings := ExtractStringsFromUserInput(standard, []PathPart{}, 0) + + if _, ok := urlStrings[claim]; !ok { + t.Errorf("url-safe token: claim %q not extracted", claim) + } + if _, ok := stdStrings[claim]; !ok { + t.Errorf("standard-base64 token: claim %q not extracted -> WAF bypass", claim) + } +}