From b72e076fc7982a9f17956cd76e7d3a3e483e3c73 Mon Sep 17 00:00:00 2001 From: Tigah <88289044+TBX3D@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:52:12 -0700 Subject: [PATCH] fix(js): bound next.js manifest fetch with the scan timeout (#323) GetPagesRouterScripts fetched _buildManifest.js with httpx.Client(0), which sets no client timeout, so a slow or hostile manifest host could hang the whole scan on the response read. thread the caller's scan timeout through instead, matching every other fetch in the package. --- internal/scan/js/frameworks/next.go | 9 +++---- internal/scan/js/frameworks/next_test.go | 30 +++++++++++++++++++++++- internal/scan/js/scan.go | 2 +- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/internal/scan/js/frameworks/next.go b/internal/scan/js/frameworks/next.go index 86e7600..60b8ff9 100644 --- a/internal/scan/js/frameworks/next.go +++ b/internal/scan/js/frameworks/next.go @@ -29,6 +29,7 @@ import ( "net/http" "regexp" "strings" + "time" urlutil "github.com/projectdiscovery/utils/url" "github.com/vmfunc/sif/internal/httpx" @@ -41,7 +42,7 @@ var nextPagesRegex = regexp.MustCompile(`\[("([^"]+\.js)"(,?))`) // cannot exhaust memory. const maxManifestSize = 5 * 1024 * 1024 -func GetPagesRouterScripts(scriptUrl string) ([]string, error) { +func GetPagesRouterScripts(scriptUrl string, timeout time.Duration) ([]string, error) { baseUrl, err := urlutil.Parse(scriptUrl) if err != nil { return nil, err @@ -53,9 +54,9 @@ func GetPagesRouterScripts(scriptUrl string) ([]string, error) { return nil, err } - // no timeout in scope here; 0 matches the previous DefaultClient behavior - // while still routing through the shared transport (proxy/headers/rate-limit). - resp, err := httpx.Client(0).Do(req) + // use the caller's scan timeout so a slow or hostile manifest host cannot + // hang the whole scan; a zero timeout would read with no deadline. + resp, err := httpx.Client(timeout).Do(req) if err != nil { fmt.Println(err) return nil, err diff --git a/internal/scan/js/frameworks/next_test.go b/internal/scan/js/frameworks/next_test.go index f22ddb6..e917e29 100644 --- a/internal/scan/js/frameworks/next_test.go +++ b/internal/scan/js/frameworks/next_test.go @@ -18,6 +18,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" ) func TestGetPagesRouterScriptsReadsPastLongLine(t *testing.T) { @@ -31,7 +32,7 @@ func TestGetPagesRouterScriptsReadsPastLongLine(t *testing.T) { })) defer srv.Close() - scripts, err := GetPagesRouterScripts(srv.URL + "/_buildManifest.js") + scripts, err := GetPagesRouterScripts(srv.URL+"/_buildManifest.js", 5*time.Second) if err != nil { t.Fatalf("GetPagesRouterScripts: %v", err) } @@ -48,3 +49,30 @@ func TestGetPagesRouterScriptsReadsPastLongLine(t *testing.T) { t.Errorf("want both early.js and late.js, got %v", scripts) } } + +func TestGetPagesRouterScriptsHonorsTimeout(t *testing.T) { + // a slow manifest host must not hang the scan: the fetch has to give up + // once the caller's timeout elapses instead of reading with no deadline. + release := make(chan struct{}) + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + <-release + w.Write([]byte(`["late.js"]`)) + })) + defer srv.Close() + defer close(release) + + done := make(chan error, 1) + go func() { + _, err := GetPagesRouterScripts(srv.URL+"/_buildManifest.js", 100*time.Millisecond) + done <- err + }() + + select { + case err := <-done: + if err == nil { + t.Fatal("expected a timeout error from the slow manifest host, got nil") + } + case <-time.After(5 * time.Second): + t.Fatal("GetPagesRouterScripts did not honor the timeout and hung") + } +} diff --git a/internal/scan/js/scan.go b/internal/scan/js/scan.go index 93d8753..0219fed 100644 --- a/internal/scan/js/scan.go +++ b/internal/scan/js/scan.go @@ -120,7 +120,7 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin for _, script := range scripts { if strings.Contains(script, "/_buildManifest.js") { log.Info("Detected Next.JS pages router! Getting all scripts from %s", script) - nextScripts, err := frameworks.GetPagesRouterScripts(script) + nextScripts, err := frameworks.GetPagesRouterScripts(script, timeout) if err != nil { spin.Stop() return nil, err