Skip to content

fix(engine): resolve memory and goroutine leaks in embedded engine usage (#7503) - #7508

Closed
ThryLox wants to merge 26 commits into
projectdiscovery:mainfrom
ThryLox:fix/embedded-engine-memory-leak
Closed

fix(engine): resolve memory and goroutine leaks in embedded engine usage (#7503)#7508
ThryLox wants to merge 26 commits into
projectdiscovery:mainfrom
ThryLox:fix/embedded-engine-memory-leak

Conversation

@ThryLox

@ThryLox ThryLox commented Jun 27, 2026

Copy link
Copy Markdown

Summary

Fixes #7503 by implementing the required leak-prevention cleanup mechanisms outlined in #7502 for long-running embedded engines.

Key Changes

  1. Size-Bounded HTTP-to-HTTPS Tracker: Replaced the unbounded sync.Map in HTTPToHTTPSPortTracker (pkg/protocols/http/httpclientpool/http_to_https_tracker.go) with a size-bounded expirable LRU cache (4096 entries max, 24h TTL) and added Purge().
  2. Per-Host Rate Limiter Pool Cleanup: Updated protocolstate.Close() (pkg/protocols/common/protocolstate/state.go) to release per-host rate-limit pool goroutines and purge the HTTP-to-HTTPS tracker on shutdown.
  3. Template Cache Purging: Updated NucleiEngine.Close() / closeInternal() (lib/sdk.go) and Parser (pkg/templates/parser.go) to purge parsed and compiled template caches on engine close.

knakul853 and others added 26 commits June 13, 2026 21:13
…iscovery#7455)

* fix(hosterrorscache): skip hosts that consistently time out

Count ErrKindNetworkTemporary (request timeouts) toward MaxHostError so a
host that times out on every request is skipped, instead of being probed by
every template. Reset-on-success still protects slow-but-alive hosts, so only
consecutive timeouts with no successful response reach the threshold.

Closes projectdiscovery#7454

* fix(hosterrorscache): count rawhttp i/o timeouts toward host skip

rawhttp/unsafe templates surface read timeouts as a plain-string error
("ReadStatusLine: ... i/o timeout") that errkit cannot classify, so it
reaches the regex fallback. The regex did not list i/o timeout, so a host
that timed out on every rawhttp request was never skipped. Add i/o timeout
to the pattern. Verified: 30 unsafe templates vs one unresponsive host went
from 271s with no skip to skipping after the threshold.

* fix(hosterrorscache): reset on success and ignore parent-context cancellation

Addresses review feedback on the timeout-skip change:

- The non-clustered HTTP path never reset the host-errors cache on a
  successful response (markHostError ignores nil, success branch only
  incremented progress), so timeout counts were not consecutive and a live
  host with intermittent timeouts could be skipped. Add markHostSuccess and
  call it on a successful request, mirroring the clustered path.

- context.DeadlineExceeded classifies as ErrKindNetworkTemporary, so a
  failure from the parent scan context being cancelled/deadlined would count
  as a host failure. Ignore failures in MarkFailedOrRemove when the caller's
  context is already done; success still resets.

Tests: parent-context cancellation not counted, non-consecutive timeouts do
not skip, and the HTTP path resets the cache on success.

* fix(hosterrorscache): reset host cache on success across all HTTP paths

The timeout-counting change is global (in the cache), but reset-on-success was
only wired into the sequential HTTP path. The parallel, race and pipeline paths
counted failures without resetting on success, so a live host with intermittent
timeouts could be falsely skipped in those modes.

Consolidate mark-and-reset into recordHostResult and call it from every HTTP
execution path (sequential, parallel, race, pipeline). The SPM handler invokes
the result callback for every request outcome including success, so resetting
there is sufficient.

* test(http): add test for the parallel generated-host case

Signed-off-by: Dwi Siswanto <git@dw1.io>

* fix(http): use generated input when record host errors

The async HTTP paths checked generated requests
with the updated input, but recorded host-error
results with the original input. If a generated
URL changed the effective host/port, `Check()` and
`MarkFailedOrRemove()` operated on different cache
keys.

So record race, parallel, and pipeline results with
the same updated input used for the request to
keep timeout counts and success resets attached to
the host that was actually checked.

Signed-off-by: Dwi Siswanto <git@dw1.io>

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: Dwi Siswanto <git@dw1.io>
…7463)

Bumps the modules group with 3 updates: [github.com/projectdiscovery/retryablehttp-go](https://github.com/projectdiscovery/retryablehttp-go), [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) and [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck).


Updates `github.com/projectdiscovery/retryablehttp-go` from 1.3.14 to 1.3.15
- [Release notes](https://github.com/projectdiscovery/retryablehttp-go/releases)
- [Commits](projectdiscovery/retryablehttp-go@v1.3.14...v1.3.15)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.84 to 0.2.85
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](projectdiscovery/wappalyzergo@v0.2.84...v0.2.85)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.39 to 1.2.40
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](projectdiscovery/cdncheck@v1.2.39...v1.2.40)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/retryablehttp-go
  dependency-version: 1.3.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.85
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.40
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* attempting per-host http client pool

* close idle conns

* fixing idle conn leak

* lint

* ignore http internal routines

* fixing tests

* lowering flaky http requirement

* fixing open gate issue

* adding perf tests

* pool rework
…tdiscovery#7323) (projectdiscovery#7465)

* fix: preserve explicit target port in network templates (fixes projectdiscovery#7323)

Problem
-------
When a user specifies a port explicitly on the command line, e.g.:

  nuclei -target TARGET:80 -t network/cves/2001/CVE-2001-1473.yaml

UseNetworkPort() was overriding port 80 with the template's port (22).
This happened because reservedPorts (80, 443, 8080, …) were always
replaced by the template port, regardless of whether the port was
deliberately chosen by the operator or merely implied by the URL scheme.

As a result, services running on non-standard ports (SSH on 80, FTP on
443, etc.) were silently scanned on the wrong port, or the connection
was refused, and the target was effectively invisible to Nuclei.

Root cause
----------
UseNetworkPort() treated a bare 'host:80' the same as 'http://host:80'.
In the former the operator explicitly chose port 80; in the latter the
port was implied by the http:// scheme.

Fix
---
Only replace a reserved port when the input contains a URL scheme
('://'), indicating the port was scheme-implied.

A bare 'host:port' form means the operator deliberately chose that port
and it is preserved unchanged.

Regression tests
----------------
Seven table-driven cases added to contextargs_test.go covering:
- No port in input  → template port used (existing behaviour)
- Explicit non-reserved port → preserved
- Bare host:80 (key regression) → preserved
- http://host:80 (scheme-implied) → replaced
- http://host (no port) → template port used
- Empty template port → no-op
- Explicit port == template port → unchanged

Fixes projectdiscovery#7323

* expand tests

* fix tests

---------

Co-authored-by: XananasX7 <xananasX7@users.noreply.github.com>
Co-authored-by: Mzack9999 <mzack9999@protonmail.com>
…8 input (projectdiscovery#7464)

Upgrade github.com/projectdiscovery/govaluate from
v0.0.0-20260504230327-80320480bb6e to
v0.0.0-20260615100919-5ee2581bbf7e to consume the fix merged in
projectdiscovery/govaluate#4.

The govaluate lexer advanced strPosition by utf8.RuneLen(utf8.RuneError)
== 3 for invalid bytes, even though only 1 byte was consumed. This
caused byte-offset drift past the actual string length and a
`slice bounds out of range` panic in readUntilFalse.

Nuclei's expressions.Evaluate only caught govaluate errors, not panics,
so targets returning invalid UTF-8 in response data could crash the
entire process.

Fixes projectdiscovery#7462
* .

* .

* adding pre-flight

* add conn reuse

* making pre-flight optional

* adding per-host httpclient + ratelimit

* add conn reuse stats

* ignoring leak in lru

* adding global connection pooling with sharding

* lint

* addressing comments

* fix lint: disambiguate retryablehttp URL access

retryablehttp.Request embeds *urlutil.URL, so .URL.String() resolved to
the wrong type and tripped staticcheck QF1008. Use .Request.URL to read
the underlying http.Request URL for rate limiting and pooling keys.

* fix race

* regression harness

* ci tuning

* address review comments

- preflight: resolve via MetaInput.Target()/CustomIP, gate banner on Silent
- http: drop dead Priority 4 branch in AnalyzeConnectionReuse

* add tests

* host stats

* reuse timing

---------

Co-authored-by: Ice3man <nizamulrana@gmail.com>
Hoist the unsigned code-template guard ahead of the dast branch so it applies on every load path.
Route every mysql DSN through a sandbox that drops allowAllFiles unless -lfa is set, matching the fs.ReadFile restriction.
* chore(deps): strip dependencies

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore: drop stripped dependencies

Signed-off-by: Dwi Siswanto <git@dw1.io>

* tests(integration): reduce external service flakiness

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(types): replace govalidator ASCII check

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(trackers): update go-github import path

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore: use our PD wrapper

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore: use our PD cache packages

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(utils): use x/net publicsuffix

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(input): decode YAML without invopop/yaml

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(yaml): route callers through pkg/utils/yaml

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(json): route callers through pkg/utils/json

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactpr(scan): replace Echo handlers with net/http

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(server): replace Echo with net/http

Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(fuzzplayground): replace Echo server with net/http

Signed-off-by: Dwi Siswanto <git@dw1.io>

* test: update subdomains testdata

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore(yaml): satisfy lints

Signed-off-by: Dwi Siswanto <git@dw1.io>

* fix codeql

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: Mzack9999 <mzack9999@protonmail.com>
`highlightAsciiSection` built a regex like `(.\n*)(.\n*)` for every
non-printable byte in a matcher snippet, intending the `.` as the
literal character hex.Dump puts in the ASCII column. The regex engine
treated it as "any char". On the first pass this over-highlights every
character in the row; on the second highlight pass (when more than one
matcher hit), the now ANSI-coloured row is full of 2-character
fragments like "\x1b[", "32", "0m" that all match. Each
strings.ReplaceAll in the highlight loop then inflates the buffer
3-10x, producing exponential growth that pegs CPU and OOMs the
process within seconds.

Triggered by any TCP/network template with >=2 binary matchers of
different lengths over a response containing non-printable bytes, run
with -debug / -debug-resp / -store-resp.

Adds a regression test that fails (hangs >5s) on the bug and passes
in <10ms with the fix.
)

Move the `reqBuilder.Write(dataInBytes)` call after the hex decode so
the logged bytes match what is actually written to the connection.
…projectdiscovery#7491)

Bumps the workflows group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: workflows
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…7490)

Bumps the modules group with 3 updates: [github.com/projectdiscovery/gologger](https://github.com/projectdiscovery/gologger), [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) and [github.com/projectdiscovery/cdncheck](https://github.com/projectdiscovery/cdncheck).


Updates `github.com/projectdiscovery/gologger` from 1.1.70 to 1.1.71
- [Release notes](https://github.com/projectdiscovery/gologger/releases)
- [Commits](projectdiscovery/gologger@v1.1.70...v1.1.71)

Updates `github.com/projectdiscovery/wappalyzergo` from 0.2.85 to 0.2.86
- [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases)
- [Commits](projectdiscovery/wappalyzergo@v0.2.85...v0.2.86)

Updates `github.com/projectdiscovery/cdncheck` from 1.2.40 to 1.2.41
- [Release notes](https://github.com/projectdiscovery/cdncheck/releases)
- [Commits](projectdiscovery/cdncheck@v1.2.40...v1.2.41)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/gologger
  dependency-version: 1.1.71
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/wappalyzergo
  dependency-version: 0.2.86
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: modules
- dependency-name: github.com/projectdiscovery/cdncheck
  dependency-version: 1.2.41
  dependency-type: indirect
  update-type: version-update:semver-patch
  dependency-group: modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
`PGClient.ExecuteQuery` built the connection URL
by interpolating the dbname directly into the path.
A value containing '?' could start the query string
and inject lib/pq options such as `sslrootcert`
before the appended `sslmode=disable`.

Build the URL from escaped userinfo, path and query
values so dbname stays part of the dbname.

Signed-off-by: Dwi Siswanto <git@dw1.io>
…very#7489)

Move template execution requirements into a shared
capability table that maps each capability to its
flag, stats key, and template predicate. Use the
shared model from template loading, workflow
parsing, request compilation, and runner stats
display so file, self-contained, headless, code,
DAST, and global matcher gates stay in sync.

Signed-off-by: Dwi Siswanto <git@dw1.io>
…#7494)

LDAP was the only JS protocol client that reached
the dialer without an `IsHostAllowed` check.
Validate ldap, ldaps, and cldap hosts before
connecting, and treat ldapi as loopback so
restricted local network access rejects Unix
socket LDAP before the socket path is opened.

Signed-off-by: Dwi Siswanto <git@dw1.io>
…covery#7480)

go-ora accepts `TRACE {FILE,DIR}`-style DSN options
and may create those files while opening the conn.
Normalize those paths and reject outside paths
unless `-allow-local-file-access` is enabled.

Signed-off-by: Dwi Siswanto <git@dw1.io>
The MSSQL helpers put `dbName` directly in the URL
query as the database parameter. A template could
include '&' in the database name and append driver
options such as certificate/Kerberos file paths.

Build the URL through a shared helper and query-
escape the database value so it stays part of the
database parameter.

Signed-off-by: Dwi Siswanto <git@dw1.io>
…ry#7482)

`OutputFile` was treated as a goimpacket option
instead of a fs sink. Since goimpacket writes the
ccache itself, krbforge had to enforce nuclei's
local file policy before building the ticket
config.

Add execution-aware JS wrappers, normalize
relative output paths into the template sandbox,
and deny outside paths unless local file access is
enabled. Keep empty output as in-memory only by
passing "-".

Signed-off-by: Dwi Siswanto <git@dw1.io>
…very#7459)

* fuzz: add parser harnesses for raw requests & templates

Add go-fuzz harnesses for raw request and template
parsing.

The new coverage exercises input raw request
parsing, HTTP raw request parsing across safe and
unsafe modes, and YAML/JSON template parsing with
lightweight compile-time validation. Seed corpora
cover common ingestion shapes, host reconstruction,
full URL paths, empty path automerge, relative
query paths, and raw HTTP template requests.

Part of projectdiscovery#7312

Signed-off-by: Dwi Siswanto <git@dw1.io>

* fuzz(types): add fuzz coverage for raw request parsing

Add a go-fuzz harness for `ParseRawRequest` and
`ParseRawRequestWithURL`.

The seed corpus covers common raw request ingestion
shapes, including simple GET requests, form posts,
JSON bodies, and URL override cases.

Signed-off-by: Dwi Siswanto <git@dw1.io>

* fuzz(http): add fuzz coverage for raw request modes

Add a go-fuzz harness for `Parse` and
`ParseRawRequest`.

The corpus covers unsafe requests, self-contained
host reconstruction, empty path handling, path
automerge, full URL paths, and relative query paths.

Signed-off-by: Dwi Siswanto <git@dw1.io>

* fuzz(templates): add fuzz coverage for parsing & compile checks

Add a go-fuzz harness for YAML and JSON template
parsing.

Generated templates are parsed in both formats and
passed thru lightweight compile-time validation.
Helper file loading is disabled so fuzz inputs
cannot turn the compile check into filesystem
access, and no protocol execution is run.

Signed-off-by: Dwi Siswanto <git@dw1.io>

* chore: satisfy lints

Signed-off-by: Dwi Siswanto <git@dw1.io>

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
)

HTTP fuzzing does not maintain the ordered request
history used by request-condition fields. Reject
fuzzing templates that reference those fields at
validation time instead of letting them eval with
an invalid request count.

Fixes projectdiscovery#4891

Signed-off-by: Dwi Siswanto <git@dw1.io>
Signed-off-by: Dwi Siswanto <git@dw1.io>
* fix(yaml): reject recursive include chains

Track the include stack while expanding YAML
includes and fail when a file is seen again or the
nesting limit is reached, keeping circular
includes from recursing until the process panics.

Also handle includes at the start of the buffer,
where the old padding lookup could slice from -1.

Signed-off-by: Dwi Siswanto <git@dw1.io>

* fix indentation

* fix typo

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: Mzack9999 <mzack9999@protonmail.com>
…age (projectdiscovery#7503)

Addresses memory and goroutine accumulation during long-running embedded engine usage as outlined in projectdiscovery#7503 / projectdiscovery#7502: 1. Replaced unbounded sync.Map in HTTPToHTTPSPortTracker with a size-bounded expirable LRU cache (4096 entries max, 24h TTL) and added Purge(). 2. Updated protocolstate.Close() to release per-host rate-limit pool goroutines and purge HTTP-to-HTTPS tracker entries. 3. Updated NucleiEngine.Close() / closeInternal() to purge parsed and compiled template caches on engine shutdown.
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 819193e7-e07b-49c9-85b5-3b4cfb1fdd84

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants