Skip to content

Commit a7f769b

Browse files
authored
fix(scan): read full body in js scanners (#166)
the page scanner and the next.js manifest parser both reassembled the response body line by line with a default bufio.Scanner (64k token cap), so a line past the cap (common with minified or inlined js) silently halted the read and dropped every script and route reference after it. read the whole body instead, capped at 5mb. parsing the raw page bytes also fixes script tags split across a newline, which the old line-joined reassembly merged and missed.
1 parent fa3223a commit a7f769b

3 files changed

Lines changed: 67 additions & 17 deletions

File tree

internal/scan/js/frameworks/next.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
package frameworks
2424

2525
import (
26-
"bufio"
2726
"context"
2827
"fmt"
28+
"io"
2929
"net/http"
3030
"regexp"
3131
"strings"
@@ -37,6 +37,10 @@ import (
3737
// nextPagesRegex matches JavaScript file references in Next.js build manifest.
3838
var nextPagesRegex = regexp.MustCompile(`\[("([^"]+\.js)"(,?))`)
3939

40+
// maxManifestSize caps the build manifest read so a huge or hostile file
41+
// cannot exhaust memory.
42+
const maxManifestSize = 5 * 1024 * 1024
43+
4044
func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
4145
baseUrl, err := urlutil.Parse(scriptUrl)
4246
if err != nil {
@@ -58,13 +62,14 @@ func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
5862
}
5963
defer resp.Body.Close()
6064

61-
var sb strings.Builder
62-
scanner := bufio.NewScanner(resp.Body)
63-
scanner.Split(bufio.ScanLines)
64-
for scanner.Scan() {
65-
sb.WriteString(scanner.Text())
65+
body, err := io.ReadAll(io.LimitReader(resp.Body, maxManifestSize))
66+
if err != nil {
67+
fmt.Println(err)
68+
return nil, err
6669
}
67-
manifestText := sb.String()
70+
// the manifest ships minified on one line; strip line breaks so the regex
71+
// treats a (rare) pretty-printed one the same as the minified form.
72+
manifestText := strings.NewReplacer("\r", "", "\n", "").Replace(string(body))
6873

6974
list := nextPagesRegex.FindAllStringSubmatch(manifestText, -1)
7075

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 frameworks
14+
15+
import (
16+
"bufio"
17+
"net/http"
18+
"net/http/httptest"
19+
"strings"
20+
"testing"
21+
)
22+
23+
func TestGetPagesRouterScriptsReadsPastLongLine(t *testing.T) {
24+
// a manifest token past bufio's 64k cap must not truncate the read and
25+
// drop the script references that follow it.
26+
huge := strings.Repeat("x", bufio.MaxScanTokenSize+1)
27+
manifest := `["early.js"]` + "\n" + huge + "\n" + `["late.js"]`
28+
29+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
30+
w.Write([]byte(manifest))
31+
}))
32+
defer srv.Close()
33+
34+
scripts, err := GetPagesRouterScripts(srv.URL + "/_buildManifest.js")
35+
if err != nil {
36+
t.Fatalf("GetPagesRouterScripts: %v", err)
37+
}
38+
39+
found := func(needle string) bool {
40+
for _, s := range scripts {
41+
if strings.Contains(s, needle) {
42+
return true
43+
}
44+
}
45+
return false
46+
}
47+
if !found("early.js") || !found("late.js") {
48+
t.Errorf("want both early.js and late.js, got %v", scripts)
49+
}
50+
}

internal/scan/js/scan.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
package js
1414

1515
import (
16-
"bufio"
1716
"context"
1817
"io"
1918
"net/http"
@@ -64,6 +63,10 @@ func (r *JavascriptScanResult) SupabaseFindings() []SupabaseFinding {
6463
return out
6564
}
6665

66+
// maxHTMLBodySize caps how much of a page we read for script extraction so a
67+
// huge or hostile response cannot exhaust memory.
68+
const maxHTMLBodySize = 5 * 1024 * 1024
69+
6770
func JavascriptScan(url string, timeout time.Duration, threads int, logdir string) (*JavascriptScanResult, error) {
6871
log := output.Module("JS")
6972
log.Start()
@@ -90,15 +93,7 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
9093
}
9194
defer resp.Body.Close()
9295

93-
var sb strings.Builder
94-
scanner := bufio.NewScanner(resp.Body)
95-
scanner.Split(bufio.ScanLines)
96-
for scanner.Scan() {
97-
sb.WriteString(scanner.Text())
98-
}
99-
html := sb.String()
100-
101-
doc, err := htmlquery.Parse(strings.NewReader(html))
96+
doc, err := htmlquery.Parse(io.LimitReader(resp.Body, maxHTMLBodySize))
10297
if err != nil {
10398
return nil, err
10499
}

0 commit comments

Comments
 (0)