Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions lib/request-processor/helpers/tryDecodeAsJWT.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand Down
53 changes: 53 additions & 0 deletions lib/request-processor/helpers/tryDecodeAsJWT_differential_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}