Files
sif/internal/scan/js/probe2_test.go
T
TigahandGitHub c1b7c7c027 fix(js): supabase jwt base64url decode and per-project error isolation (#342)
* fix(js): stop the scanning spinner on early script-parse errors

htmlquery.Parse and QueryAll failures returned before spin.Stop(), leaving
the "Scanning JavaScript files" spinner running forever on those paths while
every other error branch in the function stops it first.

* fix(js): stop reporting quoted regex-flag literals as endpoints

Quoted regex flag combos like "/gi" or "/su" in ordinary JS (e.g.
str.replace("/gi", x)) matched the root-relative path alternative in
endpointRegex and were reported as endpoints. Denylist the known flag
bigrams by exact match instead of raising minEndpointLen, so real short
endpoints like /v1, /me, /ws still extract normally.

* fix(js): keep supabase findings from other projects on a per-project error

ScanSupabase iterates every JWT found in a script and can discover several
distinct supabase projects. Four error paths inside that loop (reading the
signup response, parsing it, fetching the openapi spec, and a missing
Paths field) returned nil and the whole error, discarding any results
already collected for earlier, successfully scanned projects. Skip the
broken project with continue instead so the scan still returns what it
found.

* test(js): drop flaky wall-clock redos timing probe

go's regexp is re2 and linear by construction; the timing threshold added
only flake under -race load, not coverage. the regex-flag-literal false
positive it sat next to is guarded by TestRegexFlagEndpointsNotReported.
2026-07-22 12:54:58 -07:00

69 lines
1.9 KiB
Go

package js
import (
"testing"
"time"
)
func TestProbe_NoPanicOnBinary(t *testing.T) {
inputs := [][]byte{
{}, {0x00}, {0xff, 0xfe, 0xfd},
[]byte("ey."), []byte(`"ey.a"`), []byte(`"eyJ.a.b"`),
[]byte("\xff\xfe invalid utf8 \x80\x81 token=\"x\""),
[]byte(`"//"`), []byte(`"/"`), []byte(`"./"`),
}
// binary with an embedded jwt-ish token to reach the decode path
inputs = append(inputs, []byte(`x="ey`+"\xff\xff"+`.aa.bb"`))
for _, in := range inputs {
s := string(in)
func() {
defer func() {
if r := recover(); r != nil {
t.Errorf("panic on %q: %v", s, r)
}
}()
_ = ExtractEndpoints(s, "https://example.com/a.js")
_ = ScanSecrets(s, "https://example.com/a.js")
_, _ = ScanSupabase(s, "https://example.com/a.js", time.Second)
}()
}
t.Log("no panics across binary/truncated/multibyte/empty inputs")
}
// regression: the 7 quoted regex-flag literals must never be reported as
// endpoints, but short real endpoints in the same shape (/xx or /xxx) must
// still extract normally.
func TestRegexFlagEndpointsNotReported(t *testing.T) {
fps := []string{"/gi", "/gm", "/gs", "/gu", "/gy", "/mi", "/su"}
for _, f := range fps {
got := ExtractEndpoints(`x.replace("`+f+`","")`, "https://ex.com/a.js")
if len(got) > 0 {
t.Errorf("regex-flag literal %q wrongly reported as endpoint: %v", f, got)
}
}
}
func TestShortRealEndpointsStillExtracted(t *testing.T) {
cases := []struct {
content string
want string
}{
{`fetch("/v1")`, "https://ex.com/v1"},
{`fetch("/me")`, "https://ex.com/me"},
{`fetch("/ws")`, "https://ex.com/ws"},
{`fetch("/db")`, "https://ex.com/db"},
}
for _, c := range cases {
got := ExtractEndpoints(c.content, "https://ex.com/a.js")
found := false
for _, e := range got {
if e == c.want {
found = true
}
}
if !found {
t.Errorf("expected %q preserved as an endpoint, got %v", c.want, got)
}
}
}