Nuclei Scanner Guide: Template-Based Vulnerability Scanning & CI Integration

Nuclei Scanner Guide: Template-Based Vulnerability Scanning & CI Integration

Nuclei is a fast, template-based vulnerability scanner from ProjectDiscovery. Unlike ZAP or Burp, Nuclei runs predefined YAML templates — thousands of them for CVEs, misconfigurations, exposed panels, and default credentials. The template model makes it easy to write custom checks for your application's specific security requirements.

Why Nuclei

Speed — Nuclei scans thousands of hosts with thousands of templates in parallel. A full scan of a single web application typically takes under 5 minutes.

Templates as code — security checks live in version-controlled YAML files. Your custom security requirements become templates that run in CI.

Community templates — the nuclei-templates repository has 8,000+ templates covering CVEs, exposed config files, default credentials, and common misconfigs.

Low false positives — templates are specific. Unlike heuristic scanners, Nuclei matches exact conditions.

Installation

# Go
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest

<span class="hljs-comment"># Homebrew
brew install nuclei

<span class="hljs-comment"># Docker
docker pull projectdiscovery/nuclei:latest

<span class="hljs-comment"># Update templates
nuclei -update-templates

Basic Usage

# Scan a target with all default templates
nuclei -u https://staging.example.com

<span class="hljs-comment"># Scan with specific severity
nuclei -u https://staging.example.com -severity critical,high

<span class="hljs-comment"># Scan with specific tags
nuclei -u https://staging.example.com -tags cve,exposed-panels,misconfig

<span class="hljs-comment"># Scan multiple targets from file
nuclei -l targets.txt -severity critical,high

<span class="hljs-comment"># Output to file
nuclei -u https://staging.example.com -o nuclei-results.json -json

Template Categories

The nuclei-templates repository organizes templates by type:

Category What It Tests
cves/ Specific CVE checks (known vulnerabilities in named software)
exposed-panels/ Admin panels, monitoring dashboards left exposed
default-logins/ Default credentials for routers, CMSs, databases
misconfigurations/ Security header gaps, CORS misconfigs, open redirects
technologies/ Software fingerprinting and version detection
exposures/ Config files, .git, .env, backup files exposed on web
fuzzing/ Generic injection testing for SQL, XSS, SSRF

Writing Custom Templates

This is where Nuclei's value multiplies. Write templates for your application's specific security requirements.

Template Structure

id: template-unique-id

info:
  name: Descriptive Name
  author: your-name
  severity: high        # info, low, medium, high, critical
  description: What this template checks for.
  tags: custom,security

requests:
  - method: GET
    path:
      - "{{BaseURL}}/endpoint"
    
    matchers:
      - type: word
        words:
          - "sensitive-string"
        part: body

Check for Exposed Debug Endpoints

id: debug-endpoints-exposed

info:
  name: Debug Endpoints Exposed
  author: security-team
  severity: high
  description: Debug endpoints returning internal data are accessible without authentication.
  tags: misconfig,debug,exposure

requests:
  - method: GET
    path:
      - "{{BaseURL}}/debug"
      - "{{BaseURL}}/debug/vars"
      - "{{BaseURL}}/debug/pprof"
      - "{{BaseURL}}/__debug"
      - "{{BaseURL}}/api/debug"

    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200

      - type: word
        words:
          - "goroutine"
          - "heap"
          - "cmdline"
          - "debug_info"
        condition: or
        part: body

Check for Misconfigured CORS

id: cors-origin-reflected

info:
  name: CORS Origin Reflected
  author: security-team
  severity: high
  description: CORS allows arbitrary origins  attacker domains can make authenticated requests.
  tags: cors,misconfig

requests:
  - method: GET
    path:
      - "{{BaseURL}}/api/user"

    headers:
      Origin: "https://evil.example.com"

    matchers-condition: and
    matchers:
      - type: word
        part: header
        words:
          - "Access-Control-Allow-Origin: https://evil.example.com"

      - type: word
        part: header
        words:
          - "Access-Control-Allow-Credentials: true"

Test for SQL Injection Indicators

id: sql-error-messages

info:
  name: SQL Error Messages in Response
  author: security-team
  severity: medium
  description: SQL error messages leaked in responses indicate potential injection points.
  tags: sqli,error-disclosure

requests:
  - method: GET
    path:
      - "{{BaseURL}}/api/product?id=1'"
      - "{{BaseURL}}/api/search?q=test'"

    matchers:
      - type: regex
        regex:
          - "SQL syntax.*MySQL"
          - "Warning.*mysql_.*"
          - "valid MySQL result"
          - "PostgreSQL.*ERROR"
          - "ERROR.*syntax error at or near"
          - "ORA-[0-9]{4,5}:"
          - "Microsoft.*Driver.*SQL"
          - "ODBC SQL Server Driver"
        part: body

Test Authentication Bypass

id: auth-bypass-uuid-idor

info:
  name: UUID-Based IDOR - Account Resources
  author: security-team
  severity: critical
  description: Account resources accessible by changing UUID in URL without ownership check.
  tags: idor,auth,custom

variables:
  other_user_uuid: "00000000-0000-0000-0000-000000000001"

requests:
  - method: GET
    path:
      - "{{BaseURL}}/api/accounts/{{other_user_uuid}}/documents"

    headers:
      Authorization: "Bearer {{token}}"  # Provided via -var flag

    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200

      - type: word
        words:
          - "documents"
          - "files"
        part: body
        condition: or

    extractors:
      - type: json
        name: doc_count
        json:
          - '.total'
        part: body

Run with variables:

nuclei -u https://staging.example.com \
  -t custom-templates/auth-bypass-uuid-idor.yaml \
  -var token="your-test-token"

Dynamic Extraction and Chained Requests

id: token-reuse-vulnerability

info:
  name: Password Reset Token Reusable
  author: security-team
  severity: high
  description: Password reset tokens remain valid after use.
  tags: auth,custom

requests:
  # Step 1: Request password reset
  - raw:
      - |
        POST /api/auth/forgot-password HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json

        {"email":"{{test_email}}"}

    extractors:
      - type: regex
        name: reset_token
        regex:
          - '"token":"([a-zA-Z0-9_-]+)"'
        group: 1
        internal: true

  # Step 2: Use the token (first time — should work)
  - raw:
      - |
        POST /api/auth/reset-password HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json

        {"token":"{{reset_token}}","password":"NewPassword123!"}

  # Step 3: Use the same token again (should fail, but vulnerable apps don't invalidate)
  - raw:
      - |
        POST /api/auth/reset-password HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json

        {"token":"{{reset_token}}","password":"AnotherPassword456!"}

    matchers-condition: and
    matchers:
      - type: status
        status:
          - 200

      - type: word
        words:
          - "success"
          - "password changed"
        part: body
        condition: or

CI/CD Integration

GitHub Actions

name: Nuclei Security Scan

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM

jobs:
  nuclei-scan:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.21'

      - name: Install Nuclei
        run: go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest

      - name: Update Nuclei templates
        run: nuclei -update-templates

      - name: Run Nuclei scan
        run: |
          nuclei \
            -u ${{ vars.STAGING_URL }} \
            -t nuclei-templates/ \
            -severity critical,high \
            -o nuclei-output.json \
            -json \
            -silent \
            -retries 2
        continue-on-error: true

      - name: Run custom templates
        run: |
          nuclei \
            -u ${{ vars.STAGING_URL }} \
            -t .security/nuclei-templates/ \
            -o nuclei-custom-output.json \
            -json \
            -silent
        continue-on-error: true

      - name: Check for critical/high findings
        run: |
          CRITICAL=$(jq -r 'select(.info.severity == "critical")' nuclei-output.json 2>/dev/null | wc -l)
          HIGH=$(jq -r 'select(.info.severity == "high")' nuclei-output.json 2>/dev/null | wc -l)
          
          echo "Critical findings: $CRITICAL"
          echo "High findings: $HIGH"
          
          if [ "$CRITICAL" -gt 0 ] || [ "$HIGH" -gt 0 ]; then
            echo "::error::Critical or high severity vulnerabilities found!"
            jq -r '"\(.info.severity | ascii_upcase): \(.info.name) - \(.matched-at)"' \
              nuclei-output.json 2>/dev/null
            exit 1
          fi

      - name: Upload Nuclei results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: nuclei-scan-results
          path: |
            nuclei-output.json
            nuclei-custom-output.json

Docker-Based CI

# docker-compose.security.yml
services:
  nuclei:
    image: projectdiscovery/nuclei:latest
    volumes:
      - ./nuclei-results:/results
      - ./.security/nuclei-templates:/custom-templates
    command: >
      -u ${TARGET_URL}
      -t /root/nuclei-templates
      -t /custom-templates
      -severity critical,high,medium
      -o /results/nuclei-output.json
      -json
      -retries 2
TARGET_URL=https://staging.example.com docker-compose -f docker-compose.security.yml run --rm nuclei

Managing False Positives

Template-Level Filtering

Run specific template IDs that are relevant to your stack:

# Only run templates for your tech stack
nuclei -u https://staging.example.com \
  -t cves/ \
  -<span class="hljs-built_in">id CVE-2021-44228,CVE-2022-22965 \
  -severity critical

Exclude Known False Positives

# Exclude templates that don't apply
nuclei -u https://staging.example.com \
  -exclude-tags fuzzing \
  -exclude-id exposed-panel-wordpress,wp-login

Using .nuclei-ignore

Create .nuclei-ignore in your templates directory:

# Templates to skip — confirmed false positives for this environment
- id: wordpress-login-page    # Not WordPress
- id: phpmyadmin-panel        # Not using phpMyAdmin
- id: git-config-exposure     # .git is excluded from webroot by nginx

Confidence-Based Filtering

Only run high-confidence templates to reduce noise in CI:

nuclei -u https://staging.example.com \
  -tags misconfig,exposure,default-logins \
  -severity high,critical \
  -no-interactsh  # Skip templates requiring DNS callbacks

Organizing Custom Templates

Structure custom templates alongside your application code:

.security/
  nuclei-templates/
    auth/
      idor-account-resources.yaml
      password-reset-token-reuse.yaml
      session-fixation.yaml
    api/
      cors-misconfiguration.yaml
      rate-limiting-bypass.yaml
      json-injection.yaml
    infrastructure/
      debug-endpoints.yaml
      admin-panel-exposed.yaml
      internal-api-exposed.yaml
    README.md   # Document what each template checks and why

Treat custom templates like tests — review them in PRs, require approval for changes to auth templates, and run them as part of the deployment gate.

Nuclei vs Other Scanners

Tool Strength Best For
Nuclei Speed, custom templates, CVE coverage CI/CD gates, custom security requirements
OWASP ZAP Deep active scanning, authenticated crawling Comprehensive security audits
SQLMap SQL injection depth and exploitation Targeted SQL injection testing
Trivy Container image CVEs Infrastructure security

In practice, run Nuclei for every PR (fast, template-based), and ZAP full scans weekly or before major releases (slower but deeper). They're complementary.

Read more