Is there an existing issue for this?
Current Behavior
init() reads .nuclei-ignore before the installer writes it, in the same function.
The only read that feeds template filtering is lib/sdk_private.go:140, inside applyRequiredDefaults:
e.opts.ExcludeTags = append(e.opts.ExcludeTags, config.ReadIgnoreFile().Tags...)
applyRequiredDefaults has exactly one caller — init() line 199. Later in that same init(), the installer creates/updates the file:
line 349: latestIgnoreHash, _ := installer.NucleiSDKVersionCheck()
line 379: err = tm.UpdateIfOutdated() // writes templates + .nuclei-ignore
init() spans 146–358 on v3.11.1 (read 188, writes 338/360), so the read always precedes the write. Consequence: on any host where .nuclei-ignore does not already exist, the first engine constructed reads nothing and runs with an empty ExcludeTags. It executes the templates the deny-list exists to block — dos, bruteforce, fuzz, local, txt-service.
ExcludeTags is frozen per engine and never refreshed, so the file appearing seconds later has no effect on that engine. Every subsequent engine re-reads the file, so it self-heals after the first — which is why it is easy to miss.
The blast radius is larger than "one engine" for SDK consumers that construct one engine per protocol group and reuse it across a scan: a single leaking engine can cover every HTTP template for that whole scan.
Expected Behavior
The deny-list should be in force for the first engine too. Either read the ignore file after the installer has had a chance to create/update it, or ensure it exists before reading. The installer already knows how to write it.
Steps To Reproduce
Controlled A/B on one host, same binary, same config, same targets — the only variable is whether .nuclei-ignore existed at engine construction.
- Start from a container/host with no nuclei config dir (fresh writable layer, or
rm -rf ~/.config/nuclei ~/nuclei-templates), with updateTemplates enabled so the SDK installs templates itself.
- Run a scan that constructs a thread-safe engine and executes HTTP templates against a target that trips a
fuzz-tagged template (we used waf-fuzz, tags: waf,fuzz,fuzzing,vuln).
- Observe:
.nuclei-ignore is written during that first engine's init(), after the read.
- Run the same scan again, now that the file exists.
Timeline from our run:
12:12:23Z container started (fresh writable layer, no nuclei config dir)
12:14:30Z scan 1 starts; engine 1 init -> applyRequiredDefaults reads a MISSING .nuclei-ignore
12:15:12Z same init, later: installer writes .nuclei-ignore
12:15:24Z installer writes nuclei-templates
12:41:41Z waf-fuzz (fuzz-tagged) fires and is persisted -- still engine 1
14:12:57Z scan 2 starts, ignore file now present
Relevant log output
# scan 1 (.nuclei-ignore absent at engine construction)
fuzz-tagged templates detected = 8 across 4 hosts
total detections = 13
# scan 2 (.nuclei-ignore present), same binary/config/targets
fuzz-tagged templates detected = 0
total detections = 5
# all 4 of the hosts that tripped waf-fuzz in scan 1 still produced
# non-deny-listed detections in scan 2, so they were scanned, not skipped
Environment
- OS: Linux (containerised deployment; also applies to any host without a pre-existing ignore file)
- Nuclei: v3.11.1 used as a library; ordering also present on
dev @ a34f810f
- Go: go1.26.0
Anything else?
Environments that pre-provision .nuclei-ignore out-of-band (e.g. a shared volume populated before the process starts) never see this, which is why it can hide for a long time. It surfaces on any freshly provisioned host, and on containerised deployments it recurs on every redeploy that resets the filesystem — not just the first install.
Happy to open a PR. The smallest fix is to move the ignore-file merge in applyRequiredDefaults to after the installer runs, or to re-apply it once the installer has completed.
Related, same area, both already filed: #7695 (per-scan WithTemplateFilters clearing the deny-list, PR #7698) and #7696 (the files: section never applied in library mode, PR #7697). This one is independent of both — it is about when the file is read rather than what is read from it or what later overwrites it.
Is there an existing issue for this?
Current Behavior
init()reads.nuclei-ignorebefore the installer writes it, in the same function.The only read that feeds template filtering is
lib/sdk_private.go:140, insideapplyRequiredDefaults:applyRequiredDefaultshas exactly one caller —init()line 199. Later in that sameinit(), the installer creates/updates the file:init()spans 146–358 onv3.11.1(read 188, writes 338/360), so the read always precedes the write. Consequence: on any host where.nuclei-ignoredoes not already exist, the first engine constructed reads nothing and runs with an emptyExcludeTags. It executes the templates the deny-list exists to block —dos,bruteforce,fuzz,local,txt-service.ExcludeTagsis frozen per engine and never refreshed, so the file appearing seconds later has no effect on that engine. Every subsequent engine re-reads the file, so it self-heals after the first — which is why it is easy to miss.The blast radius is larger than "one engine" for SDK consumers that construct one engine per protocol group and reuse it across a scan: a single leaking engine can cover every HTTP template for that whole scan.
Expected Behavior
The deny-list should be in force for the first engine too. Either read the ignore file after the installer has had a chance to create/update it, or ensure it exists before reading. The installer already knows how to write it.
Steps To Reproduce
Controlled A/B on one host, same binary, same config, same targets — the only variable is whether
.nuclei-ignoreexisted at engine construction.rm -rf ~/.config/nuclei ~/nuclei-templates), withupdateTemplatesenabled so the SDK installs templates itself.fuzz-tagged template (we usedwaf-fuzz,tags: waf,fuzz,fuzzing,vuln)..nuclei-ignoreis written during that first engine'sinit(), after the read.Timeline from our run:
Relevant log output
Environment
dev@a34f810fAnything else?
Environments that pre-provision
.nuclei-ignoreout-of-band (e.g. a shared volume populated before the process starts) never see this, which is why it can hide for a long time. It surfaces on any freshly provisioned host, and on containerised deployments it recurs on every redeploy that resets the filesystem — not just the first install.Happy to open a PR. The smallest fix is to move the ignore-file merge in
applyRequiredDefaultsto after the installer runs, or to re-apply it once the installer has completed.Related, same area, both already filed: #7695 (per-scan
WithTemplateFiltersclearing the deny-list, PR #7698) and #7696 (thefiles:section never applied in library mode, PR #7697). This one is independent of both — it is about when the file is read rather than what is read from it or what later overwrites it.