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
9 changes: 6 additions & 3 deletions internal/scan/js/frameworks/next.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ import (
"github.com/vmfunc/sif/internal/httpx"
)

// nextPagesRegex matches JavaScript file references in Next.js build manifest.
var nextPagesRegex = regexp.MustCompile(`\[("([^"]+\.js)"(,?))`)
// nextPagesRegex matches chunk paths in a Next.js build manifest. anchoring
// on the opening bracket dropped every chunk but the first in a route's
// array; matching any quoted .js literal instead would pull in non-chunk
// strings like __rewrites destinations, so we require the static/ chunk shape.
var nextPagesRegex = regexp.MustCompile(`"(static(?:\\u002[fF]|/)[^"]+\.js)"`)

// maxManifestSize caps the build manifest read so a huge or hostile file
// cannot exhaust memory.
Expand Down Expand Up @@ -76,7 +79,7 @@ func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
var scripts []string

for _, el := range list {
var script = strings.ReplaceAll(el[2], "\\u002F", "/")
var script = strings.ReplaceAll(el[1], "\\u002F", "/")
url, err := urlutil.Parse(script)
if err != nil {
continue
Expand Down
86 changes: 85 additions & 1 deletion internal/scan/js/frameworks/next_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,39 @@ import (
"testing"
)

func TestGetPagesRouterScriptsCapturesAllChunksPerRoute(t *testing.T) {
// a route array can list several chunks; every one is a real script to scan,
// not just the first element after the opening bracket.
manifest := `self.__BUILD_MANIFEST={"/":["static/chunks/pages/index-a.js","static/chunks/shared-b.js"]}`

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(manifest))
}))
defer srv.Close()

scripts, err := GetPagesRouterScripts(srv.URL + "/_buildManifest.js")
if err != nil {
t.Fatalf("GetPagesRouterScripts: %v", err)
}

found := func(needle string) bool {
for _, s := range scripts {
if strings.Contains(s, needle) {
return true
}
}
return false
}
if !found("index-a.js") || !found("shared-b.js") {
t.Errorf("want both chunks index-a.js and shared-b.js, got %v", scripts)
}
}

func TestGetPagesRouterScriptsReadsPastLongLine(t *testing.T) {
// a manifest token past bufio's 64k cap must not truncate the read and
// drop the script references that follow it.
huge := strings.Repeat("x", bufio.MaxScanTokenSize+1)
manifest := `["early.js"]` + "\n" + huge + "\n" + `["late.js"]`
manifest := `["static/early.js"]` + "\n" + huge + "\n" + `["static/late.js"]`

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(manifest))
Expand All @@ -48,3 +76,59 @@ func TestGetPagesRouterScriptsReadsPastLongLine(t *testing.T) {
t.Errorf("want both early.js and late.js, got %v", scripts)
}
}

func TestGetPagesRouterScriptsRealisticManifest(t *testing.T) {
// routes map to multi-chunk arrays, shared chunks are IIFE args, and
// non-chunk .js strings appear in __rewrites and sortedPages; only the
// former may end up in scripts, or a rewrite destination could steer a fetch.
manifest := `self.__BUILD_MANIFEST=(function(a,b,c){return{` +
`__rewrites:{afterFiles:[{"source":"/proxy/legacy.js","destination":"https://cdn.evil.example/tracker.js"}],beforeFiles:[],fallback:[]},` +
`"/":[a,b,"static/chunks/pages/index-1a2b.js"],` +
`"/_error":[a,"static/chunks/pages/_error-3c4d.js"],` +
`"/blog/[slug]":[a,b,c,"static/chunks/pages/blog/[slug]-5e6f.js"],` +
`sortedPages:["/","/_app","/_error","/blog/[slug]"],` +
`ampFirstPages:[]` +
`}}("static/chunks/webpack-9f8e.js","static/chunks/main-0d1c.js","static/chunks/framework-2b3a.js"));` +
`self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();`

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(manifest))
}))
defer srv.Close()

scripts, err := GetPagesRouterScripts(srv.URL + "/_buildManifest.js")
if err != nil {
t.Fatalf("GetPagesRouterScripts: %v", err)
}

found := func(needle string) bool {
for _, s := range scripts {
if strings.Contains(s, needle) {
return true
}
}
return false
}

// every real chunk, including the trailing IIFE-arg shared chunks
wantChunks := []string{
"static/chunks/pages/index-1a2b.js",
"static/chunks/pages/_error-3c4d.js",
"static/chunks/pages/blog/[slug]-5e6f.js",
"static/chunks/webpack-9f8e.js",
"static/chunks/main-0d1c.js",
"static/chunks/framework-2b3a.js",
}
for _, c := range wantChunks {
if !found(c) {
t.Errorf("missing chunk %q, got %v", c, scripts)
}
}

// no non-chunk .js string may leak into the fetch list
for _, bad := range []string{"legacy.js", "tracker.js", "cdn.evil.example"} {
if found(bad) {
t.Errorf("false positive: captured non-chunk %q in %v", bad, scripts)
}
}
}
Loading