Nuclei templates Nuclei templates

Writing Your First Nuclei Template: Automating Vulnerability Discovery

The official Nuclei templates repository now ships over 12,000 community-maintained checks, and ProjectDiscovery merged dozens of new CVE templates in the latest release alone — including coverage for CVE-2026-34197 (Apache ActiveMQ RCE) and CVE-2026-1731 (BeyondTrust Remote Support unauthenticated WebSocket RCE) within days of disclosure. That cadence is only possible because the template format is deliberately small. A useful Nuclei template is rarely more than 25 lines of YAML.

This guide walks through writing your first template from scratch — the structure the engine actually parses, the matcher types you’ll reach for ninety percent of the time, the DSL helpers that turn brittle string checks into real detections, and the validation steps that keep your template out of the false-positive bin. The examples target Nuclei v3.8.0, released April 18, 2026, which is the current stable line.

What a Nuclei Template Actually Is

A Nuclei template is a YAML file describing a request to send and the conditions under which a response counts as a finding. The engine reads the file, fires the request through one of its protocol handlers (HTTP, DNS, TCP, SSL, file, headless, JavaScript, code, or several others), then evaluates the response against your matchers. If they fire, you get a hit.

The format is intentionally narrow. There is no scripting layer wrapped around it, no plugin API to learn — just a declarative spec the engine interprets. That constraint is the reason the community can ship a working template for a fresh CVE in hours rather than days, and why the same file works identically across Linux, macOS, and Windows builds of the binary.

Three sections do most of the work in any template: an id field, an info block carrying metadata, and a protocol block (most often http:) holding the request and its matchers. Everything else — variables, payloads, extractors, workflows, fuzzing rules — sits on top of that spine.

Setting Up Your Environment

You need Go 1.21 or newer to install from source, or a prebuilt binary from the releases page. The canonical install command is straightforward.

QUICK START
# Install the latest stable build
go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
# Pull the community templates repo
nuclei -update-templates
# Verify version (should show v3.8.0 or newer)
nuclei -version
# Run a single template against a target
nuclei -t my-template.yaml -u https://target.example.com -debug

The -update-templates flag pulls the official nuclei-templates repository into a platform-specific config directory — on macOS that’s ~/Library/Application Support/nuclei, on Linux it’s ~/.config/nuclei, and on Windows it’s %LOCALAPPDATA%\nuclei. v3 introduced these platform-aware paths to fix breakage in cloud sandboxes and Docker setups where $HOME was unreliable.

The -debug flag is non-negotiable when developing. It prints the full request and raw response to stdout so you can see exactly what the engine sent and why a matcher did or didn’t fire.

The Skeleton of an HTTP Template

Every HTTP template Nuclei has ever shipped follows the same six-block layout. Here is the minimum viable example — a template that detects a publicly exposed .git/config file, which leaks repository metadata and sometimes credentials.

EXAMPLE TEMPLATE — exposed-git-config.yaml
id: exposed-git-configinfo: name: Exposed .git/config File author: yourname severity: medium description: | Detects publicly accessible .git/config files which expose repository structure and may contain credentials. tags: exposure,git,config metadata: max-request: 1http: – method: GET path: – “{{BaseURL}}/.git/config”matchers-condition: and matchers: – type: word part: body words: – “[core]” – “repositoryformatversion” condition: and– type: status status: – 200– type: word part: header words: – “text/plain” – “application/octet-stream” condition: or negative: false

Read it top to bottom. The id is a unique kebab-case slug — Nuclei refuses to load two templates with the same ID, and spaces are forbidden because the ID appears in machine-parseable output.

The info block is metadata. The severity field uses the standard ladder — info, low, medium, high, critical — and feeds the -severity filter flag at scan time. tags is how operators select your template at scale: nuclei -tags exposure,git will run every template tagged either way.

The http block holds the actual work. {{BaseURL}} is one of Nuclei’s built-in variables and resolves to the target URL passed via -u or -l. The engine ships dozens of these — {{Hostname}}, {{RootURL}}, {{Host}}, {{Port}} — and you can define your own with a top-level variables: block.

How Matchers Decide What’s a Finding

Matchers are where templates earn their keep. Nuclei v3 supports seven matcher types, and matchers-condition: and at the request level controls how multiple matchers combine. With and, every matcher must fire. With or (the default), any one will trigger.

MATCHER TYPES — v3 SYNTAX REFERENCE
word
Plain string substring match. Cheap, fast, your default reach.
regex
Go RE2 patterns. Use when position or structure matters.
status
HTTP response codes. Almost always pair with a body matcher.
size
Response byte length. Useful for fingerprinting fixed assets.
binary
Hex-encoded byte patterns. File magic bytes, raw protocols.
dsl
Expression language with helpers. The escape hatch when others can’t compose the logic.
xpath
XPath queries against HTML or XML response bodies. Added in v3 alongside JSON and XPath headless extractors in v3.5.0.

The part field on each matcher controls what gets searched: body, header, all, interactsh_protocol, or response-specific parts like status_code and content_length exposed through DSL. Forgetting part: header when looking for an X-Powered-By value is one of the most common bugs in first-draft templates.

The condition field inside a single matcher controls how its words or regexes combine — distinct from matchers-condition at the request level. A common mistake is setting both to and and wondering why nothing matches: every word AND every matcher must fire, which is rarely what you want.

When to Reach for the DSL Matcher

The dsl type is what separates working templates from precise ones. It accepts expressions written in ProjectDiscovery’s DSL, which exposes the response as variables (body, status_code, content_length, all_headers) and provides helper functions for string manipulation, hashing, encoding, and arithmetic.

A real example: detecting a package.json exposure where the file is served as a download rather than rendered.

- type: dsl
  dsl:
    - "contains(body, 'packages') && contains(tolower(all_headers), 'application/octet-stream') && status_code == 200"

Three independent checks compose into one matcher with proper short-circuit semantics. You get header normalization (tolower), substring checks (contains), and a status comparison in a single expression. Building the same logic with separate word and status matchers is possible but reads worse and clusters poorly when Nuclei optimizes scan execution.

DSL also unlocks helpers like len(), md5(), regex(), and compare_versions() — the last is critical for templates that need to flag a software version below a known patched release without false-positiving on already-fixed installs. Mistakes in version comparisons are routinely cited in nuclei-templates issues, including a recent fix for CVE-2026-25892 where flawed version logic was throwing false positives on patched Adminer instances (PR #15462).

Extractors and Multi-Step Logic

Matchers tell you whether to report. Extractors pull data out of the response for the report itself, or for use in a follow-up request. They share the same type taxonomy (regex, kval, xpath, json, dsl) and the same part field.

Extractors become essential when a template chains requests. A common pattern: hit /api/version to grab the build number, then hit /admin/dashboard only if the version is below the patched release. Nuclei supports this via the req-condition flag and internal: true extractors, which capture data without printing it to the user.

For a first template, you don’t need this. For a second or third, especially anything targeting a specific CVE, you almost certainly will.

Validating Before You Ship

A template that hasn’t been tested against both vulnerable and patched targets is not a template — it’s a guess that happens to be valid YAML. The official TEMPLATE-CREATION-GUIDE.md in the nuclei-templates repo prescribes a three-target validation: run against a known-vulnerable instance, a patched instance, and a similar-but-different application. Pass all three, and you have something worth submitting.

The minimum local workflow:

VALIDATION COMMANDS
# Static syntax check (no requests sent)
nuclei -t my-template.yaml -validate
# Run against vulnerable instance, full debug output
nuclei -t my-template.yaml -u http://vuln.local -debug
# Run against patched instance — should produce no findings
nuclei -t my-template.yaml -u http://patched.local -debug
# Verbose response inspection
nuclei -t my-template.yaml -u http://target -debug-resp -v

-validate is your first call after every edit. It catches indentation drift, unknown matcher types, missing required fields, and ID collisions before any traffic leaves your machine. Run it on save if your editor supports it.

Pitfalls That Burn Everyone the First Time

Indentation. YAML is whitespace-sensitive and Nuclei’s parser surfaces errors with line numbers but not always the underlying cause. Mixed tabs and spaces, two-space versus four-space inconsistency, and missing dashes on list items account for most “template won’t load” reports. Use a YAML linter; the official guide explicitly recommends running one before submission.

Matcher conditions confused with request-level conditions. A condition: and inside a single word matcher requires every word to be present in the same response part. A matchers-condition: and at the request level requires every distinct matcher to fire. They’re independent, and conflating them produces templates that match nothing or everything.

Over-trusting status codes. Many vulnerable apps return 200 OK for everything, including 404 pages. Anchor on body content, not on status. Conversely, some return 403 or 401 on the very endpoints you’re trying to fingerprint — read the actual response with -debug before assuming.

Not handling false positives in version detection. The nuclei-templates issue tracker is full of templates that fire on string fragments matching unrelated software. The fix is usually a dsl matcher with compare_versions() plus a body anchor that confirms you’re actually looking at the target product.

Ignoring rate limits and template load. A template with twenty paths and four matchers each fires eighty operations per target. Set max-request honestly in the metadata block; it’s used by scan planners and CI integrations to budget runs.

A Note on Template Signing and Recent Security Advisories

Nuclei v3.8.0, released April 18, 2026, shipped two relevant security fixes worth understanding before you write or run third-party templates. GHSA-29rg-wmcw-hpf4 addressed JavaScript template behavior to respect the -allow-local-file-access flag in require calls, and GHSA-jm34-66cf-qpvr tightened expression evaluation so only template-authored expressions are evaluated. Both reflect a pattern you should internalize: Nuclei templates are code, not configuration. The official repository signs templates and the engine validates signatures by default — disabling that with -disable-template-signature is reserved for templates you wrote yourself or have audited line by line. Don’t pipe random YAML from the internet into your scanner without this discipline.

FAQ

How long should my first template take to write? Once you’ve installed Nuclei and have a target with a known issue to detect, expect 20 to 40 minutes for a single-request template, mostly spent in the debug-iterate loop. The YAML itself is rarely more than ten minutes of typing.

Do I have to learn Go to write templates? No. The Go runtime is what executes templates, but template authors only write YAML and DSL expressions. The DSL is closer to a spreadsheet formula than a programming language.

How do I get my template into the official repo? Open a pull request to projectdiscovery/nuclei-templates against main. Maintainers expect the three-target validation, sensible severity and tags, an info.description, and — for CVE templates — proper metadata fields including the CVE ID, CVSS score, and an NVD reference. AI-assisted tagging now runs on incoming PRs, but human review still gates merges.

What about the AI flag for generating templates? The -ai flag generates a draft template from a natural-language description. It’s useful as a starting point for the structure, but the output needs the same validation any hand-written template requires — and probably more, since model-generated matchers tend toward the over-broad.

Where to Go Next

Write a detection template before you write an exploitation one. Detections — exposures, default credentials, fingerprints — teach you the engine’s primitives without the noise of payload encoding, redirects, and session handling. Once a few of those work end to end, move to multi-step templates with extractors and req-condition, then to fuzzing templates that use payloads: and attack types like clusterbomb. The code and javascript protocols are last; they unlock real power but require a meaningful jump in care, especially around signing.

The official template repository is your reference manual. Pick a template close to what you’re building, read it, then write yours alongside it. Nearly every pattern you’ll need is already in there, written by someone who hit the same wall a year ago.

Add a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Cybersecurity intelligence delivered directly to your inbox.

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Advertisement