yellow.git


yellow

~ opinionated web scanning pipeline for pentesters in a hurry
git clone https://github.com/djnnvx/yellow.git
log | files | refs | archive | README | LICENSE

commit d5b50f6ea74160047c96e97ff2ad03a39e825824
parent 1a6cb4767bc1fc728f31a227349774d863ba88cb
Author: djnn <[email protected]>
Date: Sat, 8 Aug 2026 23:57:47 +0900

fix(scan): katana drops out-of-scope URLs before they reach nuclei/gobuster

Diffstat:
Mscan/katana.go | 45+++++++++++++++++++++++++++++++++++++++++----
Ascan/katana_test.go | 30++++++++++++++++++++++++++++++

2 files changed, 71 insertions(+), 4 deletions(-)

diff --git a/scan/katana.go b/scan/katana.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"math"
+	"net/url"
 	"os"
 	"sort"
 	"strings"
@@ -17,8 +18,11 @@ import (
 	"github.com/projectdiscovery/katana/pkg/engine/hybrid"
 	"github.com/projectdiscovery/katana/pkg/output"
 	"github.com/projectdiscovery/katana/pkg/types"
+	"github.com/projectdiscovery/katana/pkg/utils/scope"
 )
 
+const katanaFieldScope = "rdn"
+
 type Katana struct {
 	Depth    int
 	Duration time.Duration
@@ -37,16 +41,32 @@ func (k *Katana) Run(ctx *core.Context) error {
 		fmt.Println("[+] katana: no system chrome found, headless engine will download one on first run")
 	}
 
+	// katana outputs URLs found past MaxDepth without checking scope
+	// (engine/common/base.go), so third-party hosts reach OnResult and end up in
+	// ctx.URLs, where nuclei/gobuster/secrets would scan them. Re-apply katana's
+	// own check on the way out.
+	target, err := url.Parse(ctx.Domain)
+	if err != nil {
+		fmt.Printf("[!] katana: invalid target %s: %v\n", ctx.Domain, err)
+		return nil
+	}
+	scoper, err := scope.NewManager(nil, nil, katanaFieldScope, false)
+	if err != nil {
+		fmt.Printf("[!] katana: could not build scope manager: %v\n", err)
+		return nil
+	}
+
 	var (
-		mu   sync.Mutex
-		seen = map[string]bool{}
-		urls []string
+		mu      sync.Mutex
+		seen    = map[string]bool{}
+		urls    []string
+		dropped int
 	)
 
 	options := &types.Options{
 		MaxDepth:           k.Depth,
 		CrawlDuration:      k.Duration,
-		FieldScope:         "rdn",
+		FieldScope:         katanaFieldScope,
 		BodyReadSize:       math.MaxInt,
 		Timeout:            10,
 		TimeStable:         1, // 0 makes rod's WaitDOMStable panic on NewTicker(0)
@@ -75,6 +95,11 @@ func (k *Katana) Run(ctx *core.Context) error {
 				return
 			}
 			seen[result.Request.URL] = true
+
+			if !inScope(scoper, result.Request.URL, target.Hostname()) {
+				dropped++
+				return
+			}
 			urls = append(urls, result.Request.URL)
 		},
 	}
@@ -114,6 +139,18 @@ func (k *Katana) Run(ctx *core.Context) error {
 	}
 
 	ctx.URLs = urls
+	if dropped > 0 {
+		fmt.Printf("[+] katana: dropped %d out-of-scope URL(s)\n", dropped)
+	}
 	fmt.Printf("[SCAN %s] katana crawled %d URL(s) in %s\n\n", ctx.Domain, len(urls), outfile)
 	return nil
 }
+
+func inScope(scoper *scope.Manager, rawURL, rootHostname string) bool {
+	u, err := url.Parse(rawURL)
+	if err != nil {
+		return false
+	}
+	ok, err := scoper.Validate(u, rootHostname)
+	return err == nil && ok
+}
diff --git a/scan/katana_test.go b/scan/katana_test.go
@@ -0,0 +1,30 @@
+package scan
+
+import (
+	"testing"
+
+	"github.com/projectdiscovery/katana/pkg/utils/scope"
+)
+
+func TestInScope(t *testing.T) {
+	scoper, err := scope.NewManager(nil, nil, katanaFieldScope, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cases := map[string]bool{
+		"https://djnn.sh/":                            true,
+		"https://evil.djnn.sh/a/b":                    true,
+		"https://services.djnn.sh/x":                  true,
+		"https://github.com/djnnvx":                   false,
+		"https://cdnjs.cloudflare.com/ajax/libs/x.js": false,
+		"https://www.youtube.com/watch?v=1":           false,
+		"::::not a url":                               false,
+	}
+
+	for raw, want := range cases {
+		if got := inScope(scoper, raw, "djnn.sh"); got != want {
+			t.Errorf("%s: got %v, want %v", raw, got, want)
+		}
+	}
+}