Files
sif/internal/scan/stdout_bypass_test.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

45 lines
2.0 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"os"
"strings"
"testing"
)
// TestModulesDoNotPrintDirectlyToStdout guards against status/error lines that
// bypass the output sink via a raw fmt.Println(os.Stdout, ...): under
// -concurrency>1 those interleave/tear with lines the rest of the run writes
// through output.Info/Error, since only the sink is wrapped with a lock. every
// module should route its chrome through the output package instead.
func TestModulesDoNotPrintDirectlyToStdout(t *testing.T) {
files := []string{
"cloudstorage.go",
"subdomaintakeover.go",
}
for _, f := range files {
data, err := os.ReadFile(f)
if err != nil {
t.Fatalf("reading %s: %v", f, err)
}
src := string(data)
if strings.Contains(src, "fmt.Println(") {
t.Errorf("%s still calls fmt.Println directly, want output.Info/Error/ScanStart instead", f)
}
if strings.Contains(src, "os.Stdout") {
t.Errorf("%s references os.Stdout directly, want the output sink instead", f)
}
}
}