Skip to content
Open
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
44 changes: 31 additions & 13 deletions internal/scan/js/supabase.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
Expand All @@ -41,6 +42,34 @@ type supabaseJwtBody struct {
Role *string `json:"role"`
}

// parseSupabaseJwtBody decodes the claims segment of a jwt. jwt payloads are
// unpadded base64url, so decode with that alphabet and fall back to the padded
// variant for emitters that pad; RawStdEncoding would reject any payload whose
// base64 lands on the url-safe - or _ characters.
func parseSupabaseJwtBody(token string) (*supabaseJwtBody, error) {
parts := strings.Split(token, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("jwt has %d segments, want 3", len(parts))
}
decoded, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
decoded, err = base64.URLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("base64url decode jwt body: %w", err)
}
}
var body *supabaseJwtBody
if err := json.Unmarshal(decoded, &body); err != nil {
return nil, fmt.Errorf("unmarshal jwt body: %w", err)
}
// a literal json null unmarshals into a nil pointer with no error; guard so
// callers can dereference the result without a nil panic.
if body == nil {
return nil, errors.New("jwt body is json null")
}
return body, nil
}

type supabaseScanResult struct {
ProjectId string `json:"project_id"`
ApiKey string `json:"api_key"`
Expand Down Expand Up @@ -163,20 +192,9 @@ func ScanSupabase(jsContent string, jsUrl string, timeout time.Duration) ([]supa
jwts = slices.Compact(jwts)

for _, jwt := range jwts {
parts := strings.Split(jwt, ".")
body := parts[1]

decoded, err := base64.RawStdEncoding.DecodeString(body)
if err != nil {
supabaselog.Debugf("Failed to decode JWT %s: %s", body, err)
continue
}

supabaselog.Debugf("JWT body: %s", decoded)
var supabaseJwt *supabaseJwtBody
err = json.Unmarshal(decoded, &supabaseJwt)
supabaseJwt, err := parseSupabaseJwtBody(jwt)
if err != nil {
supabaselog.Debugf("Failed to json parse JWT %s: %s", jwt, err)
supabaselog.Debugf("Failed to parse JWT %s: %s", jwt, err)
continue
}

Expand Down
62 changes: 62 additions & 0 deletions internal/scan/js/supabase_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package js

import (
"encoding/base64"
"testing"
)

func TestParseSupabaseJwtBody(t *testing.T) {
// claims segment whose base64url encoding contains both - and _; decodes to
// {"ref":"|Z7>2V[qx?fw0","role":"anon"}. RawStdEncoding rejects it outright.
urlSafeSeg := "eyJyZWYiOiJ8Wjc-MlZbcXg_ZncwIiwicm9sZSI6ImFub24ifQ"

stdJSON := []byte(`{"ref":"mjrnzxqptwubhklsdvca","role":"anon"}`)
rawSeg := base64.RawURLEncoding.EncodeToString(stdJSON)
paddedSeg := base64.URLEncoding.EncodeToString(stdJSON)

// json null unmarshals into a nil pointer without error; the decoder must
// surface it as an error so ScanSupabase does not nil-deref the result.
nullSeg := base64.RawURLEncoding.EncodeToString([]byte("null"))
// valid claims without ref/role must decode cleanly with nil fields.
noClaimsSeg := base64.RawURLEncoding.EncodeToString([]byte(`{"iss":"supabase"}`))

cases := []struct {
name string
token string
wantErr bool
wantRef string // only checked when the case sets a non-empty value
}{
{"url-safe payload", "hdr." + urlSafeSeg + ".sig", false, "|Z7>2V[qx?fw0"},
{"unpadded base64url", "hdr." + rawSeg + ".sig", false, "mjrnzxqptwubhklsdvca"},
{"padded base64url", "hdr." + paddedSeg + ".sig", false, "mjrnzxqptwubhklsdvca"},
{"too few segments", "hdr.sig", true, ""},
{"invalid base64", "hdr.!!!!.sig", true, ""},
{"json null body", "hdr." + nullSeg + ".sig", true, ""},
{"no ref or role", "hdr." + noClaimsSeg + ".sig", false, ""},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
body, err := parseSupabaseJwtBody(tc.token)
if tc.wantErr {
if err == nil {
t.Fatalf("parseSupabaseJwtBody(%q) = nil err, want error", tc.token)
}
return
}
if err != nil {
t.Fatalf("parseSupabaseJwtBody(%q) error: %v", tc.token, err)
}
// a valid decode must never yield a nil body; callers dereference it.
if body == nil {
t.Fatalf("parseSupabaseJwtBody(%q) = nil body, nil err", tc.token)
}
if tc.wantRef == "" {
return
}
if body.ProjectId == nil || *body.ProjectId != tc.wantRef {
t.Fatalf("ProjectId = %v, want %q", body.ProjectId, tc.wantRef)
}
})
}
}
Loading