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.
This commit is contained in:
Tigah
2026-07-22 12:52:12 -07:00
committed by GitHub
parent 22935328b5
commit b72e076fc7
3 changed files with 35 additions and 6 deletions
+5 -4
View File
@@ -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
+29 -1
View File
@@ -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")
}
}
+1 -1
View File
@@ -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