Nuclei Templates: Writing Custom Security Tests for Your Application
Nuclei is the most widely-used open-source vulnerability scanner among professional security engineers. Its template-driven architecture means you can write a security check once in YAML and run it against hundreds of targets, integrate it into CI/CD pipelines, or share it with the community. Understanding how templates work — and how to write good ones — is core competency for any DevSecOps engineer.
This guide covers template anatomy, matchers and extractors, multi-step request flows, writing templates for your own application's business logic, and common mistakes that produce false positives.
Template Anatomy
Every Nuclei template is a YAML file with a mandatory id, info block, and at least one protocol block (http, dns, tcp, etc.). The simplest possible template:
id: example-exposed-debug-endpoint
info:
name: Debug Endpoint Exposed
author: your-handle
severity: medium
description: The /debug endpoint is accessible without authentication.
tags: misconfig,debug
http:
- method: GET
path:
- "{{BaseURL}}/debug"
matchers:
- type: status
status:
- 200
Run it: nuclei -t example-exposed-debug-endpoint.yaml -u https://target.com
The {{BaseURL}} variable expands to the target URL. Nuclei handles TLS, redirects, and timeouts automatically. The template fires if the status code is exactly 200.
The Info Block
The info block is not just metadata — severity drives prioritization in downstream tooling, and tags control which templates run in profile-based scans.
| Field | Values / Notes |
|---|---|
severity | info, low, medium, high, critical |
tags | Comma-separated. Controls -tags filter. Common: sqli, xss, ssrf, misconfig, auth |
reference | CVE links, CWE numbers, writeup URLs |
classification | cvss-score, cwe-id, owasp-top-10 mappings |
remediation | Short fix description — shown in reports |
Matchers
Matchers decide whether a finding is reported. You can combine multiple matchers with AND or OR logic.
Status code matcher
matchers:
- type: status
status:
- 200
- 201
Word matcher
matchers:
- type: word
words:
- "root:x:0:0"
- "/bin/bash"
condition: and # both words must appear
part: body # check response body (default)
Regex matcher
matchers:
- type: regex
regex:
- "(?i)exception.*at.*line"
- "(?i)stack trace"
condition: or
Binary matcher
For matching raw bytes — useful for detecting deserialization gadget chains or binary protocol responses:
matchers:
- type: binary
binary:
- "aced0005" # Java serialized object magic bytes
DSL matcher
DSL matchers give you the full power of Nuclei's expression language for complex conditions:
matchers:
- type: dsl
dsl:
- "status_code == 200 && contains(body, 'admin') && !contains(body, 'login')"
- "response_time > 5000" # detect time-based SQLi
Combining matchers with AND/OR
matchers-condition: and # both matchers must fire
matchers:
- type: status
status:
- 200
- type: word
words:
- "\"role\":\"admin\""
part: body
Extractors
Extractors pull data from responses — useful for chaining requests or for informational templates that capture tokens, version numbers, or sensitive values.
extractors:
- type: regex
name: csrf_token
regex:
- 'name="csrf_token" value="([a-zA-Z0-9]+)"'
group: 1 # capture group index
Named extractors become variables you can reference in subsequent requests with {{csrf_token}}.
extractors:
- type: kval
kval:
- Set-Cookie # extracts the Set-Cookie header value
Multi-Step Flows: Chaining Requests
Real vulnerabilities often require multiple steps — authenticate, then access a privileged resource, then verify the bypass. Nuclei handles this with multiple request blocks and variable passing.
id: idor-user-profile-access
info:
name: IDOR — Access Other User Profiles
severity: high
tags: idor,auth
http:
# Step 1: Authenticate as user A
- method: POST
path:
- "{{BaseURL}}/api/auth/login"
headers:
Content-Type: application/json
body: '{"email":"test@example.com","password":"testpass"}'
extractors:
- type: json
name: auth_token
json:
- ".token"
internal: true # don't report, just store
# Step 2: Access another user's profile with user A's token
- method: GET
path:
- "{{BaseURL}}/api/users/2" # hardcoded user ID — change this
headers:
Authorization: "Bearer {{auth_token}}"
matchers:
- type: status
status:
- 200
- type: word
words:
- '"email"'
- '"role"'
condition: and
matchers-condition: and
The internal: true flag on extractors prevents intermediate values from being reported as separate findings.
Writing Templates for Application-Specific Checks
The real value of custom templates is encoding your application's specific attack surface — the things generic scanners don't know about.
Admin interface exposure check
id: admin-interface-no-auth
info:
name: Admin Interface Accessible Without Authentication
severity: critical
tags: auth,misconfig
http:
- method: GET
path:
- "{{BaseURL}}/admin"
- "{{BaseURL}}/admin/"
- "{{BaseURL}}/admin/dashboard"
- "{{BaseURL}}/admin/users"
stop-at-first-match: true
matchers-condition: and
matchers:
- type: status
status:
- 200
- type: word
words:
- "admin"
- "dashboard"
- "logout"
condition: or
part: body
- type: word
words:
- "login"
- "password"
- "authenticate"
negative: true # exclude login pages
part: body
Detecting verbose error responses
id: verbose-error-disclosure
info:
name: Verbose Error Messages Expose Stack Traces
severity: medium
tags: disclosure,misconfig
http:
- method: GET
path:
- "{{BaseURL}}/api/{{randstr}}" # trigger a 404 or 500
matchers:
- type: regex
regex:
- "(?i)(traceback|stack trace|exception in thread)"
- "(?i)(at [a-zA-Z0-9.]+\\(.*:\\d+\\))"
- "(?i)(django.core.exceptions|flask.exceptions|rails exception)"
condition: or
Sensitive file exposure
id: exposed-env-file
info:
name: .env File Publicly Accessible
severity: critical
tags: exposure,config
http:
- method: GET
path:
- "{{BaseURL}}/.env"
- "{{BaseURL}}/.env.local"
- "{{BaseURL}}/.env.production"
matchers-condition: and
matchers:
- type: status
status:
- 200
- type: regex
regex:
- "(?i)(DB_PASSWORD|API_KEY|SECRET_KEY|DATABASE_URL|REDIS_URL)"
Avoiding False Positives
Bad templates create noise that security teams start ignoring. A few rules for high-fidelity templates:
Always use negative matchers to exclude false positive pages. A 200 status code means almost nothing on modern applications that serve custom 404 pages with HTTP 200. Always combine status matchers with content checks, and use negative: true to exclude pages that merely contain the login form.
Be precise with regex anchors. A regex like error will match "Error handling", "No errors found", and "error" in the URL. Anchor patterns to context: (?i)unhandled.*exception or require surrounding context.
Use stop-at-first-match: true for path fuzzing. When you're testing multiple paths (admin, /admin/, /admin/dashboard), the first match is enough. Without this flag, Nuclei reports each matching path as a separate finding.
Validate against known-good targets before deploying. Run your template against an application where the vulnerability does not exist. If it fires, your matcher is too broad.
Fuzz Mode: Parameter Injection
Nuclei's fuzzing mode (available in v3+) lets templates inject payloads into HTTP parameters, headers, and path components:
id: reflected-xss-fuzz
info:
name: Reflected XSS via Query Parameter
severity: high
tags: xss,fuzz
http:
- pre-condition:
- type: dsl
dsl:
- "method == 'GET'"
fuzzing:
- part: query
mode: single
fuzz:
- '">'
- "javascript:alert(1)"
- "'>
"
matchers:
- type: word
words:
- '">'
part: body
Running Your Templates
# Single template against single target
nuclei -t my-template.yaml -u https://target.com
# Template directory against target list
nuclei -t ./custom-templates/ -l targets.txt
# Filter by severity
nuclei -t ./custom-templates/ -u https://target.com -severity high,critical
# Filter by tags
nuclei -t ./custom-templates/ -u https://target.com -tags auth,misconfig
# Parallel execution (increase for larger target lists)
nuclei -t ./custom-templates/ -l targets.txt -c 25
# Output formats
nuclei -t ./custom-templates/ -u https://target.com -o results.json -je
# Integrate into CI — exit code 1 on findings
nuclei -t ./custom-templates/ -u https://staging.myapp.com -severity high,critical
echo "Exit: $?"
Template Organization for Teams
If you're running templates across a team or CI/CD pipeline, structure matters:
custom-templates/
├── auth/
│ ├── admin-no-auth.yaml
│ ├── idor-user-profile.yaml
│ └── jwt-none-alg.yaml
├── config/
│ ├── exposed-env.yaml
│ ├── debug-endpoints.yaml
│ └── verbose-errors.yaml
├── injection/
│ ├── reflected-xss.yaml
│ └── sqli-error-based.yaml
└── compliance/
├── missing-security-headers.yaml
└── http-methods-allowed.yaml
Tag every template with the relevant category. This lets CI gate on specific categories (-tags auth,critical) without running the full template library on every PR build.
CI/CD Integration Pattern
A practical GitLab CI / GitHub Actions step that fails the pipeline on high-severity findings:
# .github/workflows/security.yml
- name: Nuclei Security Scan
run: |
nuclei -t ./security/custom-templates/ \
-u ${{ env.STAGING_URL }} \
-severity high,critical \
-o nuclei-results.json \
-je \
-silent
# Fail if any findings
if [ -s nuclei-results.json ]; then
echo "Security issues found:"
cat nuclei-results.json | jq '.'
exit 1
fi
Ironimo runs thousands of Nuclei templates — including the community library and templates tuned for Ironimo's scanning engine — against your application on every scan. Custom template support lets you encode your application's specific attack surface and run it continuously alongside the standard checks.
Start free scan