Files
sif/internal/scan/js/frameworks/next.go
T
TigahandGitHub a44cfb230f fix(store,logger,scan): concurrency-adjacent races and correctness fixes (#343)
* fix(logger): create log dirs and flatten url log filenames

CreateFile/Write kept the '/' from a target's url path when building the
log filename, so a target like http://host:port/ tried to open
<dir>/host:port/.log whose parent directory never existed. os.OpenFile
failed and scanTarget aborted the whole target before any scanning ran.

fold every '/' and '\\' run in the sanitized url into a single '_' via a
shared logPath helper so CreateFile and Write always resolve to the same
flat file, and mkdir the parent defensively in getWriter as a second line
of defense.

* fix(scan): route module status lines through the locking sink

subdomaintakeover, cloudstorage, and the next.js framework detector
printed their status/error lines with a bare fmt.Println straight to
os.Stdout, bypassing the output package's sink. under -concurrency>1
only the sink is lock-wrapped, so these lines could interleave or tear
against lines other targets write concurrently.

swap them for output.ScanStart / output.Error, matching every other
scanner in the package.

* fix(store): make snapshot filenames injective and writes atomic

sanitize() folds every separator run (and a literal '_') to one '_', so
distinct targets like https://a.com/x and https://a.com//x, or
host:8443/path and host_8443_path, sanitized to the identical string and
shared one snapshot file - the second target's Save silently clobbered
the first's baseline. Save's os.WriteFile also wasn't atomic, so two
targets racing on a collided path (or -concurrency>1 in general) could
interleave partial writes.

pathFor now appends 16 hex chars of the target's sha256 to the sanitized
name, so distinct targets never collide, and Save writes through a temp
file in the same dir followed by os.Rename so a reader always sees a
complete snapshot or the previous one, never a partial write.

this changes the on-disk snapshot filename scheme: existing users' -diff
baselines under the old sanitize()-only names won't be found and the
next run re-baselines from scratch (every finding reports as newly
added once). noting this as a deliberate tradeoff for a local hardening
pass rather than silently orphaning them.

* fix(store): check the deferred temp-file cleanup error

golangci-lint (errcheck) flagged the bare defer os.Remove(tmpPath) in
writeFileAtomic; wrap it so the discard is explicit.
2026-07-22 12:55:08 -07:00

96 lines
3.3 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
What we are doing is abusing a internal file in Next.js pages router called
_buildManifest.js which lists all routes and script files ever referenced in
the application within next.js, this allows us to optimise and not bruteforce
directories for routes and instead get all of them at once.
We are currently parsing this js file with regexes but that should ideally be
replaced soon.
*/
package frameworks
import (
"context"
"io"
"net/http"
"regexp"
"strings"
"time"
urlutil "github.com/projectdiscovery/utils/url"
"github.com/vmfunc/sif/internal/httpx"
"github.com/vmfunc/sif/internal/output"
)
// nextPagesRegex matches JavaScript file references in Next.js build manifest.
var nextPagesRegex = regexp.MustCompile(`\[("([^"]+\.js)"(,?))`)
// maxManifestSize caps the build manifest read so a huge or hostile file
// cannot exhaust memory.
const maxManifestSize = 5 * 1024 * 1024
func GetPagesRouterScripts(scriptUrl string, timeout time.Duration) ([]string, error) {
baseUrl, err := urlutil.Parse(scriptUrl)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, scriptUrl, http.NoBody)
if err != nil {
output.Error("%v", err)
return nil, err
}
// 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 {
output.Error("%v", err)
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, maxManifestSize))
if err != nil {
output.Error("%v", err)
return nil, err
}
// the manifest ships minified on one line; strip line breaks so the regex
// treats a (rare) pretty-printed one the same as the minified form.
manifestText := strings.NewReplacer("\r", "", "\n", "").Replace(string(body))
list := nextPagesRegex.FindAllStringSubmatch(manifestText, -1)
var scripts []string
for _, el := range list {
var script = strings.ReplaceAll(el[2], "\\u002F", "/")
url, err := urlutil.Parse(script)
if err != nil {
continue
}
if url.IsRelative {
url.Host = baseUrl.Host
url.Scheme = baseUrl.Scheme
url.Path = "/_next/" + url.Path
}
scripts = append(scripts, url.String())
}
return scripts, nil
}