Skip to content

Commit 1d7e219

Browse files
TBX3Dvmfunc
andauthored
feat(fuzz): add fuzz harnesses for jwt, secrets and yaml parsing (#340)
* refactor(modules): extract parseYAMLModuleBytes from ParseYAMLModule split the byte-parsing and validation core out of the file-reading wrapper so module definitions can be parsed from memory. behavior is unchanged: ParseYAMLModule reads the file then delegates. * test(fuzz): add harnesses for parsers and extractors cover the untrusted-input parsers that had no fuzz coverage: jwt analysis and segment decode, js secret scanning, yaml module parsing, openapi spec parsing, html title extraction and js endpoint extraction. the secrets, openapi and endpoint harnesses assert invariants (match is a substring of input, ok implies non-nil spec, results non-empty and sorted); the rest are crash-only. --------- Co-authored-by: vmfunc <vmfunc.lc@gmail.com>
1 parent 7d206ad commit 1d7e219

6 files changed

Lines changed: 210 additions & 0 deletions

File tree

internal/modules/yaml_fuzz_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package modules
14+
15+
import "testing"
16+
17+
func FuzzParseYAMLModule(f *testing.F) {
18+
f.Add([]byte("id: t\ntype: http\n"))
19+
f.Add([]byte("id: x\ntype: http\nhttp:\n matchers:\n - type: word\n words: [foo]\n"))
20+
f.Add([]byte("type: http\n"))
21+
f.Add([]byte("id: x\ntype: dns\n"))
22+
f.Add([]byte(""))
23+
f.Add([]byte("id: x\ntype: http\nhttp:\n matchers-condition: xor\n"))
24+
25+
f.Fuzz(func(t *testing.T, data []byte) {
26+
ParseYAMLModuleBytes(data)
27+
})
28+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package js
14+
15+
import (
16+
"slices"
17+
"testing"
18+
)
19+
20+
func FuzzExtractEndpoints(f *testing.F) {
21+
f.Add(`fetch("/api/users")`, "https://example.com/app.js")
22+
f.Add(`url: "https://cdn.example.com/v1/data.json"`, "")
23+
f.Add(`const p = "../relative/path"`, "https://example.com/a/b/")
24+
f.Add(`"text/html"`, "https://example.com")
25+
f.Add("", "")
26+
f.Add(`axios.get("/x").then()`, "not a url")
27+
28+
f.Fuzz(func(t *testing.T, content, baseURL string) {
29+
got := ExtractEndpoints(content, baseURL)
30+
for _, e := range got {
31+
if e == "" {
32+
t.Fatal("ExtractEndpoints returned an empty endpoint")
33+
}
34+
}
35+
if !slices.IsSorted(got) {
36+
t.Fatalf("ExtractEndpoints result not sorted: %v", got)
37+
}
38+
})
39+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package js
14+
15+
import (
16+
"strings"
17+
"testing"
18+
)
19+
20+
func FuzzScanSecrets(f *testing.F) {
21+
f.Add(`const key = "AKIAIOSFODNN7EXAMPLE"`, "https://example.com/app.js")
22+
f.Add(`apikey: "sk-1234567890abcdefghij"`, "")
23+
f.Add("ghp_0123456789abcdefghijklmnopqrstuvwxyz01", "src")
24+
f.Add("-----BEGIN RSA PRIVATE KEY-----", "")
25+
f.Add(`var token = ""`, "")
26+
f.Add("", "")
27+
f.Add("aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "js")
28+
29+
f.Fuzz(func(t *testing.T, content, srcURL string) {
30+
for _, m := range ScanSecrets(content, srcURL) {
31+
// a reported match must be a non-empty run lifted verbatim from the
32+
// input; anything else means the capture-group indexing is off
33+
if m.Match == "" {
34+
t.Fatalf("empty Match for rule %q", m.Rule)
35+
}
36+
if !strings.Contains(content, m.Match) {
37+
t.Fatalf("Match %q (rule %q) not found in input", m.Match, m.Rule)
38+
}
39+
}
40+
})
41+
}

internal/scan/jwt_fuzz_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package scan
14+
15+
import "testing"
16+
17+
func FuzzAnalyzeJWT(f *testing.F) {
18+
f.Add("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig")
19+
f.Add("eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiJ9.")
20+
f.Add("a.b.c")
21+
f.Add("..")
22+
f.Add("")
23+
f.Add("not-a-jwt")
24+
25+
f.Fuzz(func(t *testing.T, raw string) {
26+
analyzeJWT("fuzz", raw)
27+
})
28+
}
29+
30+
func FuzzDecodeJWTSegment(f *testing.F) {
31+
f.Add("eyJhbGciOiJIUzI1NiJ9")
32+
f.Add("eyJzdWIiOiIxMjM0In0")
33+
f.Add("bm90LWpzb24")
34+
f.Add("!!!!")
35+
f.Add("")
36+
f.Add("eyJhIjp7ImIiOnsiYyI6MX19fQ")
37+
38+
f.Fuzz(func(t *testing.T, seg string) {
39+
decodeJWTSegment(seg)
40+
})
41+
}

internal/scan/openapi_fuzz_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package scan
14+
15+
import "testing"
16+
17+
func FuzzParseOpenAPISpec(f *testing.F) {
18+
f.Add([]byte(`{"openapi":"3.0.0","paths":{"/x":{"get":{}}}}`))
19+
f.Add([]byte(`{"swagger":"2.0","paths":{"/y":{}}}`))
20+
f.Add([]byte("openapi: 3.0.0\npaths:\n /z:\n get: {}\n"))
21+
f.Add([]byte(`{"paths":{}}`))
22+
f.Add([]byte("not a spec"))
23+
f.Add([]byte(""))
24+
f.Add([]byte("{"))
25+
26+
f.Fuzz(func(t *testing.T, body []byte) {
27+
spec, ok := parseOpenAPISpec(body)
28+
if ok && spec == nil {
29+
t.Fatal("parseOpenAPISpec returned ok with a nil spec")
30+
}
31+
})
32+
}

internal/scan/probe_fuzz_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
3+
: :
4+
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
5+
: ▄█ █ █▀ · BSD 3-Clause License :
6+
: :
7+
: (c) 2022-2026 vmfunc, xyzeva, :
8+
: lunchcat alumni & contributors :
9+
: :
10+
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
11+
*/
12+
13+
package scan
14+
15+
import "testing"
16+
17+
func FuzzExtractTitle(f *testing.F) {
18+
f.Add([]byte("<html><head><title>Hi</title></head></html>"))
19+
f.Add([]byte("<TITLE class=x> spaced </TITLE>"))
20+
f.Add([]byte("<title>unclosed"))
21+
f.Add([]byte("<title></title>"))
22+
f.Add([]byte("no title here"))
23+
f.Add([]byte(""))
24+
f.Add([]byte("<title>a</title><title>b</title>"))
25+
26+
f.Fuzz(func(t *testing.T, body []byte) {
27+
extractTitle(body)
28+
})
29+
}

0 commit comments

Comments
 (0)