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.

FieldValues / Notes
severityinfo, low, medium, high, critical
tagsComma-separated. Controls -tags filter. Common: sqli, xss, ssrf, misconfig, auth
referenceCVE links, CWE numbers, writeup URLs
classificationcvss-score, cwe-id, owasp-top-10 mappings
remediationShort 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
← Back to blog