Secure the PipelineDASTStep 27 of 31

Nuclei — Templated DAST in CI

difficulty intermediatehands-on 25 min hands-onteardown required

87% complete

prereqs · owasp-zap-attack-your-app

concepts · templated scanning · known-issue detection · custom templates · CI gating

Nuclei is a request engine driven by YAML templates. It ships with thousands of community templates covering known CVEs, default credentials, exposed admin panels and misconfigurations — and it is fast enough to run against every environment on every deploy.

It is also the clearest demonstration in this track of a rule that has come up in every chapter: a scanner finds what somebody wrote a rule for. Nuclei just makes the boundary unusually visible, because the rules are files you can read and write yourself.

Verified against Nuclei 3.9.0, against the Range app. Output copied from a real run.

Step 1 — Run the community templates

bash
brew install nuclei

git clone https://github.com/jaybilgaye/aiopsone-range
cd aiopsone-range
docker build -f docker/Dockerfile -t range-app:vulnerable .
docker run -d --name range-dast -p 127.0.0.1:3100:3000 range-app:vulnerable

nuclei -u http://127.0.0.1:3100
text
[http-missing-security-headers:content-security-policy] [info] http://127.0.0.1:3100
[http-missing-security-headers:x-frame-options]         [info] http://127.0.0.1:3100
[http-missing-security-headers:strict-transport-security][info] http://127.0.0.1:3100
[http-missing-security-headers:permissions-policy]      [info] http://127.0.0.1:3100
[http-missing-security-headers:x-content-type-options]  [info] http://127.0.0.1:3100
[tech-detect:express]                                   [info] http://127.0.0.1:3100
[form-detection]                                        [info] http://127.0.0.1:3100
[options-method]                                        [info] http://127.0.0.1:3100 ["GET,HEAD"]

Every finding is [info]. Missing headers, technology fingerprinting, a form detected.

Meanwhile this application has an unauthenticated endpoint that returns AWS credentials as JSON, and a reflected XSS. Nuclei reported neither.

Why, and it is not a criticism. The community templates encode known issues — CVE-2021-44228 at a known path, a default Grafana login, an exposed .git directory. /debug/config is not a known path; it is a route somebody on your team wrote. No template exists because nobody outside your organisation has ever seen your application.

Filtering by tag does not change the answer:

bash
nuclei -u http://127.0.0.1:3100 -tags exposure,misconfig,xss

Still only the header findings. Ten thousand templates, zero coverage of your actual application.

Step 2 — Write the templates that do apply

A Nuclei template is a request and a set of matchers:

yaml
# templates/range-debug-endpoint.yaml
id: range-debug-config-exposure
info:
  name: Debug endpoint leaking credentials
  author: aiopsone
  severity: critical
  tags: exposure,config
http:
  - method: GET
    path:
      - "{{BaseURL}}/debug/config"
    matchers-condition: and
    matchers:
      - type: status
        status: [200]
      - type: word
        part: body
        words: ["awsAccessKeyId", "dbPassword"]
        condition: and
yaml
# templates/range-reflected-xss.yaml
id: range-reflected-xss
info:
  name: Reflected XSS in greet parameter
  author: aiopsone
  severity: high
  tags: xss
http:
  - method: GET
    path:
      - "{{BaseURL}}/greet?name=%3Cscript%3Ealert(1)%3C%2Fscript%3E"
    matchers-condition: and
    matchers:
      - type: status
        status: [200]
      - type: word
        part: body
        words: ["<script>alert(1)</script>"]
bash
nuclei -u http://127.0.0.1:3100 -t templates/
text
[range-reflected-xss] [http] [high] http://127.0.0.1:3100/greet?name=%3Cscript%3E…
[range-debug-config-exposure] [http] [critical] http://127.0.0.1:3100/debug/config

Critical and high, where ten thousand community templates produced nothing but info.

Two things make these work. matchers-condition: and means both a 200 and the expected content — a 404 page containing the word "awsAccessKeyId" will not trigger it. And the XSS matcher looks for the payload reflected unencoded; if the app escaped it to &lt;script&gt;, the match fails and the template correctly reports nothing.

Terminal showing community nuclei templates returning only info findings, and two custom templates returning critical and high

Step 3 — Where this fits against ZAP

The ZAP lesson found the SQL injection and missed the XSS, because its spider never discovered /greet. Nuclei has no spider at all — you tell it the path — so it found the XSS immediately and knows nothing about the SQL injection.

ZAP active scan Nuclei + custom templates
Discovers endpoints spiders, found 7 URLs none — you supply paths
SQL injection found not tested
Reflected XSS at /greet missed (never crawled) found
/debug/config exposure missed (never crawled) found
Runtime minutes seconds

They are complementary rather than competing. ZAP explores and attacks generically; Nuclei checks specific known things very fast. Nuclei's speed is what makes it the one you can run against production, because a GET on a path you own is not an attack.

Step 4 — Gate it

Nuclei finishes in seconds, so it fits in the pull request where ZAP's active scan does not:

yaml
# .github/workflows/dast.yml
- name: Nuclei (own templates only)
  run: |
    docker run -d --name app -p 127.0.0.1:3100:3000 app:${{ github.sha }}
    sleep 10
    nuclei -u http://127.0.0.1:3100 \
      -t ./templates/ \
      -severity critical,high \
      -no-color -stats \
      -exit-code 1

-t ./templates/ runs only yours. The community set is worth running on a schedule against deployed environments, but in a pull-request gate it is thousands of requests to learn that you still have no CSP header — a finding that will not change between commits and will train people to ignore the job.

-severity critical,high keeps the info-level noise out of a blocking check.

Step 5 — Turn every incident into a template

The habit worth forming: when something is found in production — by a pentest, a bug bounty, or an outage — write the Nuclei template that detects it before you close the ticket.

yaml
id: internal-admin-panel-unauthenticated
info:
  name: Admin panel reachable without auth
  severity: critical
http:
  - method: GET
    path: ["{{BaseURL}}/admin", "{{BaseURL}}/internal/admin"]
    matchers:
      - type: status
        status: [200]

Now it is a regression test that runs on every deploy, in every environment, forever. That is the difference between fixing an issue and ensuring it stays fixed — and after a year of doing it, your template directory is a more accurate description of your actual risks than any vendor's ruleset.

Step 6 — Clean up

bash
docker rm -f range-dast

What you learned

  • Thousands of community templates, all [info], on an app leaking AWS credentials. They encode known issues, and your application is not one.
  • Two templates written in ten minutes found both real bugs, at critical and high.
  • matchers-condition: and is what stops false positives: require the status and the content.
  • Nuclei and ZAP miss different things. ZAP found the SQLi and missed the XSS; Nuclei the reverse. Neither is a substitute.
  • Gate on -t ./templates/ only. The community set on a pull request is thousands of requests confirming a finding that never changes.
  • Write a template for every incident. After a year that directory describes your real risk better than any vendor ruleset.