Nuclei Template Creation
Authorized Use Only
Run generated templates only against targets explicitly in scope. Templates that send payloads (SSRF callbacks, command injection probes, authentication attempts) must respect the same rules of engagement as manual testing. Validate on an approved test host before scanning production.
Objectives
- Convert a confirmed finding into a YAML template with low-to-zero false positives.
- Produce a template that validates against the official JSON schema and passes
nuclei -validate. - Document severity, remediation, and references so the template is report-ready.
- Minimize request count and avoid destructive payloads.
Workflow
- Gather inputs:
- Exact request that triggers the condition (method, path, headers, body)
- Exact response signal that confirms it (status, body string, header, timing)
- CVE/CWE IDs, vendor, product, affected versions, references
- Severity justification (CVSS or business-impact rationale)
- Choose protocol and request style:
http:withmethod+pathfor simple GET/POST checkshttp:withraw:for precise control (custom headers, malformed requests, multi-step auth)dns:,ssl:,tcp:,javascript:for non-HTTP targets- Never use the deprecated
requests:key — alwayshttp:
- Build matchers for the narrowest reliable signal:
- Prefer
matchers-condition: andcombining status + body/header evidence - Use
type: wordfor literal strings,type: regexonly when patterns vary - Use
type: dslfor cross-field logic (e.g.status_code == 200 && contains(body, "x")) - Set
part:explicitly (body,header,all) — do not rely on defaults when precision matters - Add
negative: truematchers to exclude known false-positive pages
- Prefer
- Add extractors only if output is needed:
regexwithgroup:for version numbers or tokenskvalfor response headers- Mark
internal: trueif the value feeds a later request rather than report output
- Self-review against the quality checks below, then validate.
Template Skeleton
Use this as the starting structure. Remove unused blocks — do not leave empty keys.
id: vendor-product-issue-type
info:
name: Vendor Product — Issue Summary
author: your-handle
severity: info|low|medium|high|critical
description: |
One or two sentences describing what is detected and why it matters.
remediation: |
Specific fix action (patch version, config change, etc.).
reference:
- https://vendor.example/advisory
- https://nvd.nist.gov/vuln/detail/CVE-XXXX-YYYYY
classification:
cve-id: CVE-XXXX-YYYYY
cwe-id: CWE-NN
metadata:
verified: true
max-request: 1
vendor: vendor-name
product: product-name
tags: cve,cve2025,rce,vendor-name
http:
- method: GET
path:
- "{{BaseURL}}/path/to/check"
matchers-condition: and
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- "unique-string-confirming-vuln"
condition: and
extractors:
- type: regex
part: body
group: 1
regex:
- 'version["\s:]+([0-9.]+)'
Raw Request Form
Use when you need exact wire-level control (multi-step, auth chains, non-standard formatting):
http:
- raw:
- |
POST /api/login HTTP/1.1
Host: {{Hostname}}
Content-Type: application/json
{"user":"{{username}}","pass":"{{password}}"}
- |
GET /api/admin HTTP/1.1
Host: {{Hostname}}
Cookie: {{session}}
cookie-reuse: true
extractors:
- type: regex
name: session
part: header
internal: true
regex:
- 'Set-Cookie: (session=[a-f0-9]+)'
matchers:
- type: word
part: body_2
words:
- "admin_dashboard"
Suffix response parts with _N (1-indexed) to match against a specific request in a multi-request chain.
Non-HTTP Protocols
DNS
dns:
- name: "{{FQDN}}"
type: CNAME
matchers:
- type: word
words:
- "s3.amazonaws.com"
part: answer
TCP / SSL
tcp:
- address:
- "{{Host}}:{{Port}}"
inputs:
- data: "\r\n"
read-size: 2048
matchers:
- type: word
part: body
words:
- "OpenSSH"
ssl:
- address: "{{Host}}:{{Port}}"
matchers:
- type: dsl
dsl:
- 'contains(subject_cn, "internal.corp")'
- 'not_after < unix_time()' # expired cert
condition: or
Headless (Browser)
headless:
- steps:
- action: navigate
args:
url: "{{BaseURL}}/login"
- action: waitload
matchers:
- type: word
part: body
words:
- "admin panel"
Use headless: only when JavaScript rendering is required; it is much slower than http:.
Variables and Payloads
Use a variables: block to precompute values:
variables:
encoded: "{{base64('admin:admin')}}"
http:
- method: GET
path:
- "{{BaseURL}}/api/v1/secret"
headers:
Authorization: "Basic {{encoded}}"
Use payloads: with an attack type for fuzzing checks:
http:
- method: POST
path:
- "{{BaseURL}}/search"
body: "q={{payload}}"
payloads:
payload:
- "' OR 1=1--"
- "\" OR 1=1--"
attack: batteringram # one payload at a time; use clusterbomb for combos
matchers:
- type: word
words:
- "SQL syntax"
part: body
Attack types: batteringram (single list, same value per position), pitchfork (parallel lists), clusterbomb (cartesian product).
Flow Control
Use flow: to orchestrate multi-protocol or conditional request chains. Requests only run when the preceding gate returns true.
flow: http(1) && http(2)
http:
- method: GET
path:
- "{{BaseURL}}/wp-content/plugins/vuln-plugin/readme.txt"
matchers:
- type: word
words: ["Vuln Plugin"]
internal: true # gate check — suppresses output
- method: POST
path:
- "{{BaseURL}}/wp-admin/admin-ajax.php"
body: "action=exploit"
matchers:
- type: word
words: ["success"]
Output Template
# Nuclei Template: {{id}}
## Template summary
- File: `{{id}}.yaml`
- Protocol: http | dns | tcp | ssl | headless
- Severity: info | low | medium | high | critical
- Request count: N
## Detection logic
- Trigger path/condition:
- Matcher signals:
1.
2.
- Extractors (if any):
## Validation results
- `nuclei -validate`: pass | fail (list errors)
- True-positive host tested: yes | no
- True-negative host tested: yes | no
- False-positive risk notes:
## Handoff to pt-scanning
- Template path for inclusion in scan runs:
- Recommended tags/filters: `-tags`
- Any prerequisites (auth creds, interactsh server, etc.):
Validation
Use the bundled script for combined schema + lint checking:
# Validate a single template (schema + lint)
bash scripts/validate.sh ./template.yaml
# Validate + dry-run against an approved host
bash scripts/validate.sh ./template.yaml -u https://approved-test-host
# Validate all templates in a directory
bash scripts/validate.sh ./templates/
Or run nuclei directly:
nuclei -t ./template.yaml -validate
nuclei -t ./template.yaml -u https://approved-test-host -debug
Reference Material
Load these files on demand when more depth is needed:
- references/REFERENCE.md — complete DSL helper functions, all matcher/extractor types, template variables, and response-part table
- references/protocols.md — full option reference for HTTP, DNS, TCP, SSL, Headless, File, and JavaScript protocols
- assets/cve-http.yaml — annotated CVE detection template (copy and adapt)
- assets/tech-fingerprint.yaml — technology fingerprinting template
- assets/subdomain-takeover.yaml — DNS subdomain takeover detection template
Upstream reference (authoritative):
- Syntax reference:
github.com/projectdiscovery/nuclei/blob/dev/SYNTAX-REFERENCE.md - JSON schema:
github.com/projectdiscovery/nuclei/blob/dev/nuclei-jsonschema.json - Community templates:
github.com/projectdiscovery/nuclei-templates
Quality Checks
idis lowercase, hyphen-separated, and describes vendor + issue (no spaces, no generic names liketestorvuln).info.severityis justified —critical/highonly for confirmed RCE, auth bypass, or direct data exposure.metadata.max-requestmatches the actual number of requests the template sends.- Matchers require at least two independent signals (e.g. status AND body) unless a single signal is provably unique.
- No
requests:key (deprecated) — useshttp:instead. - Template passes
nuclei -validatewith no warnings. - Template was run against one known-vulnerable and one known-clean host to confirm true-positive and true-negative behavior.
- Payloads are non-destructive: no file writes, no account creation, no state mutation unless explicitly approved and documented in
description.