Skip to content

feat(core): implement lock-free honeypot detection with sync.Map (#6403)#7068

Open
astraxghost wants to merge 1 commit intoprojectdiscovery:devfrom
astraxghost:feature/issue-6403-honeypot-detector
Open

feat(core): implement lock-free honeypot detection with sync.Map (#6403)#7068
astraxghost wants to merge 1 commit intoprojectdiscovery:devfrom
astraxghost:feature/issue-6403-honeypot-detector

Conversation

@astraxghost
Copy link

@astraxghost astraxghost commented Feb 27, 2026

Fixes Issue: #6403

Description:
This PR introduces a high-performance, lock-free honeypot detection system to help users reduce noise and avoid scanning traps that yield infinite or excessive matches.

Technical Implementation:

Options & CLI: Added the -hp / -honeypot flag.

Efficiency: Implemented HoneypotTracker using sync.Map and atomic counters. This ensures zero bottlenecking during highly concurrent scans across multiple hosts.

Logic: Once a target host exceeds 15 successful template matches, the system flags it as a honeypot, logs a single warning, and skips all subsequent templates for that specific host.

UX: The warning message is triggered only once per host to maintain a clean terminal output.

Testing Performed:

Unit Tests: Verified logic in pkg/core/ and confirmed flag registration.

Manual Stress Test: Confirmed that execution stops precisely after the threshold when firing against a mock server designed to return positive matches for every request.

Compilability: Successfully built with go build ./cmd/nuclei.

Impact:
Significantly reduces false positives and resource consumption when encountering honeypots or misconfigured targets.

/claim #6403

Summary by CodeRabbit

  • New Features
    • Added --honeypot / -hp command-line flag to enable honeypot detection functionality
    • Honeypot detection automatically tracks suspected malicious inputs on a per-host basis throughout the scanning process
    • When honeypots are detected, the scanner automatically skips further template execution on those inputs to reduce noise and improve overall scanning efficiency

@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Feb 27, 2026

Neo - PR Security Review

Medium: 2

Highlights

  • Implements lock-free honeypot detection using sync.Map with atomic.Uint32 counters
  • Adds CLI flag (-hp/--honeypot) to enable honeypot detection
  • Tracks matches per host and skips further scanning after 15+ matches
Medium (2)
  • Log Injection via unsanitized host inputpkg/core/executors.go:119
    The target hostname from t.value.Input is logged directly without sanitization. If the hostname contains newline characters (\n, \r) or ANSI escape sequences, an attacker can inject fake log entries or manipulate terminal output.
  • Log Injection via unsanitized host inputpkg/core/executors.go:243
    The target hostname from value.Input is logged directly without sanitization. If the hostname contains newline characters (\n, \r) or ANSI escape sequences, an attacker can inject fake log entries or manipulate terminal output.
Security Impact

Log Injection via unsanitized host input (pkg/core/executors.go:119):
Attacker-controlled target hostnames can inject arbitrary log entries, potentially misleading incident responders, hiding scanning activity, or exploiting vulnerabilities in log parsing systems (SIEM, monitoring tools).

Log Injection via unsanitized host input (pkg/core/executors.go:243):
Attacker-controlled target hostnames can inject arbitrary log entries, potentially misleading incident responders, hiding scanning activity, or exploiting vulnerabilities in log parsing systems (SIEM, monitoring tools).

Attack Examples

Log Injection via unsanitized host input (pkg/core/executors.go:119):

User scans target list containing: "example.com\n[INF] Target safe.com is verified\n[INF] Skipping honeypot checks" - this injects fake log entries suggesting safe.com was verified when it wasn't.

Log Injection via unsanitized host input (pkg/core/executors.go:243):

User scans target list containing: "evil.com\n[SUCCESS] No vulnerabilities found\n" - this injects fake log entries to hide actual findings or create confusion in security dashboards.
Suggested Fixes

Log Injection via unsanitized host input (pkg/core/executors.go:119):

Sanitize t.value.Input before logging by stripping or escaping newlines and control characters. Use strings.ReplaceAll or a dedicated sanitization function.

Log Injection via unsanitized host input (pkg/core/executors.go:243):

Sanitize value.Input before logging by stripping or escaping newlines and control characters. Use strings.ReplaceAll or a dedicated sanitization function.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/core/executors.go` at line 119, the log message includes t.value.Input
directly without sanitization; add input sanitization before logging by
replacing newlines and control characters, for example: sanitized :=
strings.ReplaceAll(strings.ReplaceAll(t.value.Input, "\n", ""), "\r", "") and
use sanitized in the log message instead of t.value.Input to prevent log
injection attacks.

In `@pkg/core/executors.go` at line 243, the log message includes value.Input
directly without sanitization; add input sanitization before logging by
replacing newlines and control characters, for example: sanitized :=
strings.ReplaceAll(strings.ReplaceAll(value.Input, "\n", ""), "\r", "") and use
sanitized in the log message instead of value.Input to prevent log injection
attacks.
Hardening Notes
  • Consider adding a maximum size limit for the honeypotTracker sync.Map to prevent unbounded memory growth when scanning millions of unique hosts
  • Add unit tests in pkg/core/engine_test.go to verify CheckHoneypot method correctly handles concurrent access and threshold logic
  • Document the threshold value (15) as a constant in engine.go for maintainability instead of using magic number 16

Comment @neo help for available commands. · Open in Neo

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Walkthrough

This PR introduces honeypot detection functionality to reduce scanner noise. It adds a CLI flag --honeypot, extends the Options struct with DetectHoneypot, implements per-host honeypot tracking in the Engine, and integrates detection checks into template execution to skip processing when thresholds are exceeded.

Changes

Cohort / File(s) Summary
CLI & Options Configuration
cmd/nuclei/main.go, pkg/types/types.go
Adds --honeypot / -hp flag binding to Options struct and ensures DetectHoneypot is copied when duplicating options.
Honeypot Detection Implementation
pkg/core/engine.go, pkg/core/executors.go
Implements per-host honeypot tracking via honeypotTracker (sync.Map) in Engine with CheckHoneypot method; integrates detection checks and early-skip logic throughout template and target execution paths to reduce unnecessary scanning.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as CLI/Main
    participant Options as Options
    participant Engine as Engine
    participant Executors as Executors
    
    User->>CLI: Provide --honeypot flag
    CLI->>Options: Set DetectHoneypot=true
    Executors->>Engine: Call CheckHoneypot(host)
    Engine->>Engine: Track hits per host (counter)
    alt Counter > 15
        Engine-->>Executors: return true
        Executors->>Executors: Skip processing, log notice
    else Counter <= 15
        Engine-->>Executors: return false
        Executors->>Executors: Continue normal execution
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops through the honeypots with glee,
Tracking each trap at counter's decree,
When fifteen's passed, we skip with care,
Less noise, less waste—a scan most fair! 🍯✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main feature being introduced: implementing lock-free honeypot detection using sync.Map, which aligns with the primary changes across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/core/engine.go (1)

75-83: Avoid magic-number threshold drift across engine and executors.

Line 83 hardcodes 16 while executors compare > 15. Centralize these values in one constant/API to prevent behavior drift.

♻️ Suggested refactor
+const honeypotMatchThreshold uint32 = 15

 func (e *Engine) CheckHoneypot(host string) bool {
 	if !e.options.DetectHoneypot {
 		return false
 	}
 	v, _ := e.honeypotTracker.LoadOrStore(host, &atomic.Uint32{})
 	hits := v.(*atomic.Uint32).Add(1)
-	return hits == 16
+	return hits == honeypotMatchThreshold+1
 }

Then use the same threshold constant (or a helper method) in executor skip checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/core/engine.go` around lines 75 - 83, Centralize the honeypot threshold
by adding a package-level constant (e.g., HoneypotThreshold = 15) and use it in
Engine.CheckHoneypot (replace the hardcoded 16 with a comparison using the
constant, e.g., hits > HoneypotThreshold) and update any executor skip checks
that currently use `> 15` to reference the same constant; touch the
CheckHoneypot function, the honeypotTracker usage, and the executor skip logic
so both components read the single shared HoneypotThreshold symbol.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/core/executors.go`:
- Around line 117-121: The honeypot tracker currently uses raw
t.value.Input/target.Input which fragments counts by scheme/path/query/port;
update all calls that pass raw inputs into e.CheckHoneypot (and any honeypot
logging like options.Logger.Info().Msgf) to instead compute and use a normalized
host key (e.g., extract host:port or canonical host via net/url parsing and
default ports) and pass that normalizedHost to e.CheckHoneypot and log messages;
apply this normalization for every occurrence of
t.value.Input/target.Input/value.Input used in honeypot checks across this file
(including the calls around e.CheckHoneypot and the associated Logger.Info()
messages) so counting and detection are per-host.

In `@pkg/types/types.go`:
- Line 473: Update the comment that currently references "tm" to correctly
reference the actual mutex field name "m" in the types.go struct; locate the
comment near the mutex field (the line that reads "// tm is a mutex to protect
timeouts from concurrent access") and change the identifier to "m" so the
comment matches the field name and avoids confusion during maintenance.

---

Nitpick comments:
In `@pkg/core/engine.go`:
- Around line 75-83: Centralize the honeypot threshold by adding a package-level
constant (e.g., HoneypotThreshold = 15) and use it in Engine.CheckHoneypot
(replace the hardcoded 16 with a comparison using the constant, e.g., hits >
HoneypotThreshold) and update any executor skip checks that currently use `> 15`
to reference the same constant; touch the CheckHoneypot function, the
honeypotTracker usage, and the executor skip logic so both components read the
single shared HoneypotThreshold symbol.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9142eae and 1642d2e.

📒 Files selected for processing (4)
  • cmd/nuclei/main.go
  • pkg/core/engine.go
  • pkg/core/executors.go
  • pkg/types/types.go

Comment on lines +117 to +121
if match {
if e.CheckHoneypot(t.value.Input) {
e.options.Logger.Info().Msgf("[INF] Target %s appears to be a honeypot, skipping further scans\n", t.value.Input)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== MetaInput definition =="
rg -n -C3 --type=go 'type\s+MetaInput\s+struct|Input\s+string' pkg/core/

echo
echo "== Constructors / assignments to MetaInput.Input =="
rg -n -C2 --type=go 'MetaInput\s*{[^}]*Input:|\.Input\s*=' pkg/core/

echo
echo "== Honeypot tracker call sites =="
rg -n -C2 --type=go 'CheckHoneypot\(|honeypotTracker\.Load\(' pkg/core/

echo
echo "== honeypotTracker data structure definition =="
rg -n -C2 --type=go 'honeypotTracker' pkg/core/

Repository: projectdiscovery/nuclei

Length of output: 3920


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find MetaInput struct definition =="
find . -name "*.go" -type f | xargs grep -l "type MetaInput struct" | head -5

echo
echo "== Search for MetaInput in executors.go context =="
rg -n "MetaInput" pkg/core/executors.go

echo
echo "== Full honeypot tracker section in executors.go =="
sed -n '110,130p' pkg/core/executors.go
sed -n '170,190p' pkg/core/executors.go
sed -n '210,230p' pkg/core/executors.go
sed -n '235,250p' pkg/core/executors.go

echo
echo "== CheckHoneypot method definition =="
rg -A 10 "func.*CheckHoneypot" pkg/core/executors.go

echo
echo "== honeypotTracker definition and usage =="
rg -n "honeypotTracker" pkg/core/executors.go

Repository: projectdiscovery/nuclei

Length of output: 2974


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== MetaInput struct definition =="
cat pkg/protocols/common/contextargs/metainput.go

echo
echo "== CheckHoneypot method full implementation =="
rg -A 20 "func.*CheckHoneypot" pkg/core/executors.go

echo
echo "== honeypotTracker field definition and initialization =="
rg -B 5 -A 5 "honeypotTracker" pkg/core/executors.go | head -50

Repository: projectdiscovery/nuclei

Length of output: 5277


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Look for CheckHoneypot in all files =="
rg -n "func.*CheckHoneypot" pkg/core/

echo
echo "== honeypotTracker initialization =="
rg -B 10 "honeypotTracker.*=" pkg/core/executors.go | head -40

echo
echo "== Examine full executeTemplatesOnTarget method =="
sed -n '200,250p' pkg/core/executors.go

Repository: projectdiscovery/nuclei

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CheckHoneypot implementation =="
sed -n '77,120p' pkg/core/engine.go

echo
echo "== honeypotTracker field in Engine struct =="
rg -B 5 "honeypotTracker" pkg/core/engine.go

echo
echo "== Engine struct definition =="
rg -B 2 -A 30 "type Engine struct" pkg/core/engine.go | head -50

Repository: projectdiscovery/nuclei

Length of output: 1818


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== How Input is populated - look at main input handling =="
rg -n "\.Input\s*=" pkg/core/executors.go pkg/input/ 2>/dev/null | head -20

echo
echo "== Examples of how targets are provided to Engine =="
rg -B 5 "Engine.*Execute" pkg/core/executors.go | head -40

echo
echo "== Confirmation: all honeypot checks use .Input directly =="
rg -n "CheckHoneypot\|honeypotTracker\.Load" pkg/core/executors.go

Repository: projectdiscovery/nuclei

Length of output: 1174


Use a normalized host key for honeypot tracking, not raw Input.

The tracker key is currently value.Input/target.Input. If inputs vary by scheme/path/query/port, counts split across keys and "per-host" honeypot detection under-triggers.

🐛 Suggested direction
- if e.CheckHoneypot(t.value.Input) {
+ if e.CheckHoneypot(normalizeHoneypotKey(t.value.Input)) {
    e.options.Logger.Info().Msgf("[INF] Target %s appears to be a honeypot, skipping further scans\n", t.value.Input)
  }

- v, ok := e.honeypotTracker.Load(scannedValue.Input)
+ v, ok := e.honeypotTracker.Load(normalizeHoneypotKey(scannedValue.Input))

And apply the same normalization for target.Input / value.Input in all honeypot checks.

Also applies to: 178-184, 217-222, 241-245

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/core/executors.go` around lines 117 - 121, The honeypot tracker currently
uses raw t.value.Input/target.Input which fragments counts by
scheme/path/query/port; update all calls that pass raw inputs into
e.CheckHoneypot (and any honeypot logging like options.Logger.Info().Msgf) to
instead compute and use a normalized host key (e.g., extract host:port or
canonical host via net/url parsing and default ports) and pass that
normalizedHost to e.CheckHoneypot and log messages; apply this normalization for
every occurrence of t.value.Input/target.Input/value.Input used in honeypot
checks across this file (including the calls around e.CheckHoneypot and the
associated Logger.Info() messages) so counting and detection are per-host.

// this overrides the default/derived one
timeouts *Timeouts
// m is a mutex to protect timeouts from concurrent access
// tm is a mutex to protect timeouts from concurrent access
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix mutex comment identifier mismatch.

Line 473 references tm, but the actual field is m. This is misleading during maintenance.

✏️ Proposed fix
-	// tm is a mutex to protect timeouts from concurrent access
+	// m is a mutex to protect timeouts from concurrent access
 	m sync.Mutex
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// tm is a mutex to protect timeouts from concurrent access
// m is a mutex to protect timeouts from concurrent access
m sync.Mutex
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/types/types.go` at line 473, Update the comment that currently references
"tm" to correctly reference the actual mutex field name "m" in the types.go
struct; locate the comment near the mutex field (the line that reads "// tm is a
mutex to protect timeouts from concurrent access") and change the identifier to
"m" so the comment matches the field name and avoids confusion during
maintenance.

}
if match {
if e.CheckHoneypot(t.value.Input) {
e.options.Logger.Info().Msgf("[INF] Target %s appears to be a honeypot, skipping further scans\n", t.value.Input)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Log Injection via unsanitized host input (CWE-117) — The target hostname from t.value.Input is logged directly without sanitization. If the hostname contains newline characters (\n, \r) or ANSI escape sequences, an attacker can inject fake log entries or manipulate terminal output.

Attack Example
User scans target list containing: "example.com\n[INF] Target safe.com is verified\n[INF] Skipping honeypot checks" - this injects fake log entries suggesting safe.com was verified when it wasn't.
Suggested Fix
Sanitize t.value.Input before logging by stripping or escaping newlines and control characters. Use strings.ReplaceAll or a dedicated sanitization function.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/core/executors.go` at line 119, the log message includes t.value.Input
directly without sanitization; add input sanitization before logging by
replacing newlines and control characters, for example: sanitized :=
strings.ReplaceAll(strings.ReplaceAll(t.value.Input, "\n", ""), "\r", "") and
use sanitized in the log message instead of t.value.Input to prevent log
injection attacks.

}
if match {
if e.CheckHoneypot(value.Input) {
e.options.Logger.Info().Msgf("[INF] Target %s appears to be a honeypot, skipping further scans\n", value.Input)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Log Injection via unsanitized host input (CWE-117) — The target hostname from value.Input is logged directly without sanitization. If the hostname contains newline characters (\n, \r) or ANSI escape sequences, an attacker can inject fake log entries or manipulate terminal output.

Attack Example
User scans target list containing: "evil.com\n[SUCCESS] No vulnerabilities found\n" - this injects fake log entries to hide actual findings or create confusion in security dashboards.
Suggested Fix
Sanitize value.Input before logging by stripping or escaping newlines and control characters. Use strings.ReplaceAll or a dedicated sanitization function.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/core/executors.go` at line 243, the log message includes value.Input
directly without sanitization; add input sanitization before logging by
replacing newlines and control characters, for example: sanitized :=
strings.ReplaceAll(strings.ReplaceAll(value.Input, "\n", ""), "\r", "") and use
sanitized in the log message instead of value.Input to prevent log injection
attacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant