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-templatesBasic 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 -jsonTemplate 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: bodyCheck 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: bodyCheck 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: bodyTest 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: bodyRun 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: orCI/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.jsonDocker-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 2TARGET_URL=https://staging.example.com docker-compose -f docker-compose.security.yml run --rm nucleiManaging 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 criticalExclude Known False Positives
# Exclude templates that don't apply
nuclei -u https://staging.example.com \
-exclude-tags fuzzing \
-exclude-id exposed-panel-wordpress,wp-loginUsing .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 nginxConfidence-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 callbacksOrganizing 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 whyTreat 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.