diff --git a/pkg/catalog/loader/loader.go b/pkg/catalog/loader/loader.go index e88d30125b..554d3093b9 100644 --- a/pkg/catalog/loader/loader.go +++ b/pkg/catalog/loader/loader.go @@ -5,6 +5,7 @@ import ( "io" "net/url" "os" + "slices" "sort" "strings" "sync" @@ -786,6 +787,26 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp } } + // noteExcludedByTag surfaces a template the index filter dropped because it + // carries a tag from the .nuclei-ignore defaults. Without it the exclusion is + // silent at every verbosity level. + // + // indexFilter.ExcludeTags is a merge of CLI -exclude-tags and the ignore-file + // tags, so it must be matched against the ignore-file tags specifically: + // otherwise user-requested -exclude-tags drops would be mislabeled as + // .nuclei-ignore exclusions. + ignoreFileTags := config.ReadIgnoreFile().Tags + noteExcludedByTag := func(templatePath string, metadata *index.Metadata) { + if len(ignoreFileTags) == 0 || !slices.ContainsFunc(ignoreFileTags, metadata.HasTag) { + return + } + + stats.Increment(templates.TemplatesExcludedStats) + if config.DefaultConfig.LogAllEvents { + store.logger.Print().Msgf("[%v] %v excluded from default run using .nuclei-ignore\n", aurora.Yellow("WRN").String(), templatePath) + } + } + typesOpts := store.config.ExecutorOptions.Options concurrency := typesOpts.TemplateLoadingConcurrency if concurrency <= 0 { @@ -820,6 +841,7 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp if cachedMetadata, found := store.metadataIndex.Get(templatePath); found { metadata = cachedMetadata if !indexFilter.Matches(metadata) { + noteExcludedByTag(templatePath, metadata) return } // NOTE(dwisiswant0): else, tagFilter probably exists (for @@ -842,6 +864,7 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp } if metadata != nil && !indexFilter.Matches(metadata) { + noteExcludedByTag(templatePath, metadata) return } } diff --git a/pkg/protocols/headless/engine/page.go b/pkg/protocols/headless/engine/page.go index 80de93a51f..a6e40fb13d 100644 --- a/pkg/protocols/headless/engine/page.go +++ b/pkg/protocols/headless/engine/page.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "slices" "strings" "sync" "time" @@ -287,8 +288,26 @@ func (p *Page) addInteractshURL(URLs ...string) { p.InteractshURLs = append(p.InteractshURLs, URLs...) } +// appendRule records a request/response modification rule. The hijack handler +// reads p.rules from a separate goroutine, so writes must be synchronized. +func (p *Page) appendRule(r rule) { + p.mutex.Lock() + defer p.mutex.Unlock() + + p.rules = append(p.rules, r) +} + +// rulesSnapshot returns a copy of the current rules for lock-free iteration by +// the hijack handler (which performs network I/O and must not hold the lock). +func (p *Page) rulesSnapshot() []rule { + p.mutex.RLock() + defer p.mutex.RUnlock() + + return slices.Clone(p.rules) +} + func (p *Page) hasModificationRules() bool { - for _, rule := range p.rules { + for _, rule := range p.rulesSnapshot() { if containsAnyModificationActionType(rule.Action) { return true } diff --git a/pkg/protocols/headless/engine/page_actions.go b/pkg/protocols/headless/engine/page_actions.go index d9fcc7c451..5fad666c5d 100644 --- a/pkg/protocols/headless/engine/page_actions.go +++ b/pkg/protocols/headless/engine/page_actions.go @@ -286,7 +286,7 @@ func (p *Page) ActionAddHeader(act *Action, out ActionData) error { return err } - p.rules = append(p.rules, rule{ + p.appendRule(rule{ Action: ActionAddHeader, Part: part, Args: args, @@ -314,7 +314,7 @@ func (p *Page) ActionSetHeader(act *Action, out ActionData) error { return err } - p.rules = append(p.rules, rule{ + p.appendRule(rule{ Action: ActionSetHeader, Part: part, Args: args, @@ -337,7 +337,7 @@ func (p *Page) ActionDeleteHeader(act *Action, out ActionData) error { return err } - p.rules = append(p.rules, rule{ + p.appendRule(rule{ Action: ActionDeleteHeader, Part: part, Args: args, @@ -360,7 +360,7 @@ func (p *Page) ActionSetBody(act *Action, out ActionData) error { return err } - p.rules = append(p.rules, rule{ + p.appendRule(rule{ Action: ActionSetBody, Part: part, Args: args, @@ -383,7 +383,7 @@ func (p *Page) ActionSetMethod(act *Action, out ActionData) error { return err } - p.rules = append(p.rules, rule{ + p.appendRule(rule{ Action: ActionSetMethod, Part: part, Args: args, diff --git a/pkg/protocols/headless/engine/rules.go b/pkg/protocols/headless/engine/rules.go index 23dc6cd0c0..8f2df77c9e 100644 --- a/pkg/protocols/headless/engine/rules.go +++ b/pkg/protocols/headless/engine/rules.go @@ -17,7 +17,10 @@ func (p *Page) routingRuleHandler(httpClient *http.Client) func(ctx *rod.Hijack) return func(ctx *rod.Hijack) { // usually browsers don't use chunked transfer encoding, so we set the content-length nevertheless ctx.Request.Req().ContentLength = int64(len(ctx.Request.Body())) - for _, rule := range p.rules { + // snapshot the rules once: ExecuteActions may still be appending rules + // from another goroutine while this hijack handler runs. + rules := p.rulesSnapshot() + for _, rule := range rules { if rule.Part != "request" { continue } @@ -61,7 +64,7 @@ func (p *Page) routingRuleHandler(httpClient *http.Client) func(ctx *rod.Hijack) } } - for _, rule := range p.rules { + for _, rule := range rules { if rule.Part != "response" { continue }