Nuclei includes a powerful headless browser engine built on top of Chrome/Chromium that enables testing of modern JavaScript-heavy applications. This allows you to interact with dynamic content, execute scripts, and test complex workflows.
Overview
The headless protocol in Nuclei uses the rod library to control a headless browser, providing capabilities for:
- Navigating to URLs and rendering JavaScript
- Clicking buttons and interacting with elements
- Filling forms and submitting data
- Executing custom JavaScript code
- Taking screenshots
- Intercepting and modifying requests/responses
- Waiting for page events and conditions
Headless templates are particularly useful for testing SPAs (Single Page Applications), Progressive Web Apps, and modern JavaScript frameworks like React, Vue, and Angular.
Basic usage
Simple navigation
id: headless-basic
info:
name: Basic headless navigation
author: pdteam
severity: info
tags: headless
headless:
- steps:
- action: navigate
args:
url: "{{BaseURL}}/"
- action: waitload
matchers:
- type: word
words:
- "<html>"
id: login-form-test
info:
name: Login form interaction
author: pdteam
severity: info
headless:
- steps:
- action: navigate
args:
url: "{{BaseURL}}/login"
- action: waitload
- action: text
args:
selector: '#username'
value: 'admin'
- action: text
args:
selector: '#password'
value: 'password123'
- action: click
args:
selector: 'button[type="submit"]'
- action: waitload
matchers:
- type: word
words:
- "Welcome"
- "Dashboard"
Available actions
Navigation actions
navigate
Navigate to a URL:
- action: navigate
args:
url: "{{BaseURL}}/page"
Interaction actions
click
Click on an element:
- action: click
args:
selector: '.button'
rightclick
Right-click on an element:
- action: rightclick
args:
selector: '.menu-item'
text
Input text into a field:
- action: text
args:
selector: 'input[name="username"]'
value: 'testuser'
keyboard
Simulate keyboard actions:
- action: keyboard
args:
keys: '\r' # Press Enter
select
Select option from dropdown:
- action: select
args:
selector: 'select[name="country"]'
value: 'USA'
files
Upload files (requires -allow-local-file-access flag):
- action: files
args:
selector: 'input[type="file"]'
value: '/path/to/file.pdf'
Wait actions
waitload
Wait for the page to fully load:
waitdom
Wait for DOM content to be loaded:
waitidle
Wait for network to be idle:
waitstable
Wait for the page to be stable (no more changes):
waitfcp
Wait for First Contentful Paint:
waitfmp
Wait for First Meaningful Paint:
waitvisible
Wait for an element to become visible:
- action: waitvisible
args:
by: selector
value: '.success-message'
waitevent
Wait for a specific browser event:
- action: waitevent
args:
event: 'networkIdle'
sleep
Simple sleep for a duration:
- action: sleep
args:
duration: '2s'
Script actions
script
Execute custom JavaScript:
- action: script
name: extract
args:
code: |
() => {
return document.title;
}
HTTP manipulation actions
Set request header:
- action: setheader
args:
part: request
key: 'Authorization'
value: 'Bearer {{token}}'
Add request header:
- action: addheader
args:
part: request
key: 'X-Custom-Header'
value: 'custom-value'
Delete request header:
- action: deleteheader
args:
part: request
key: 'Referer'
setbody
Set request body:
- action: setbody
args:
part: request
value: '{"key": "value"}'
setmethod
Set request method:
- action: setmethod
args:
part: request
value: 'POST'
Extract data from elements:
- action: extract
args:
by: selector
selector: 'div.content'
extract: 'text'
getresource
Get resource content:
- action: getresource
args:
url: '{{BaseURL}}/api/data'
screenshot
Take a screenshot:
- action: screenshot
args:
to: 'screenshot.png'
Advanced examples
id: extract-urls-headless
info:
name: Extract URLs using JavaScript
author: pdteam
severity: info
headless:
- steps:
- action: navigate
args:
url: "{{BaseURL}}"
- action: waitload
- action: script
name: extract
args:
code: |
() => {
return '\n' + [...new Set(Array.from(document.querySelectorAll('[src], [href], [url], [action]')).map(i => i.src || i.href || i.url || i.action))].join('\r\n') + '\n'
}
matchers:
- type: regex
part: extract
regex:
- "https?://[^\s]+"
extractors:
- type: kval
part: extract
kval:
- extract
Cookie manipulation
id: headless-cookie-test
info:
name: Test with cookie manipulation
author: pdteam
severity: info
headless:
- steps:
- action: navigate
args:
url: "{{BaseURL}}/login"
- action: script
args:
code: |
() => {
document.cookie = "admin=true; path=/";
document.cookie = "role=administrator; path=/";
}
- action: navigate
args:
url: "{{BaseURL}}/admin"
- action: waitload
matchers:
- type: word
words:
- "Admin Panel"
File upload test
id: file-upload-test
info:
name: File upload testing
author: pdteam
severity: info
tags: headless,upload
headless:
- steps:
- action: navigate
args:
url: "{{BaseURL}}/upload"
- action: waitload
- action: files
args:
selector: 'input[type="file"]'
value: '/tmp/test.txt'
- action: click
args:
selector: 'button[type="submit"]'
- action: waitload
matchers:
- type: word
words:
- "Upload successful"
Multi-step workflow
id: multi-step-workflow
info:
name: Complex multi-step workflow
author: pdteam
severity: medium
headless:
- steps:
# Step 1: Login
- action: navigate
args:
url: "{{BaseURL}}/login"
- action: text
args:
selector: '#username'
value: '[email protected]'
- action: text
args:
selector: '#password'
value: 'password'
- action: click
args:
selector: 'button[type="submit"]'
- action: waitload
# Step 2: Navigate to settings
- action: click
args:
selector: 'a[href="/settings"]'
- action: waitload
# Step 3: Modify settings
- action: click
args:
selector: '#enable-feature'
- action: click
args:
selector: 'button.save'
- action: waitvisible
args:
by: selector
value: '.success-message'
matchers:
- type: word
words:
- "Settings saved successfully"
id: headless-header-test
info:
name: Request header modification
author: pdteam
severity: info
headless:
- steps:
- action: setheader
args:
part: request
key: 'X-Forwarded-For'
value: '127.0.0.1'
- action: addheader
args:
part: request
key: 'X-Custom-Header'
value: 'test-value'
- action: navigate
args:
url: "{{BaseURL}}/"
- action: waitload
matchers:
- type: word
words:
- "test-value"
Fuzzing with headless
Combine fuzzing with headless for dynamic testing:
id: headless-xss-fuzzing
info:
name: XSS testing with headless fuzzing
author: pdteam
severity: high
headless:
- steps:
- action: navigate
args:
url: "{{BaseURL}}/search"
fuzzing:
- part: query
type: replace
mode: single
keys:
- q
fuzz:
- "<script>alert(1)</script>"
- "<img src=x onerror=alert(1)>"
matchers:
- type: word
words:
- "<script>alert(1)</script>"
Configuration options
Page timeout
Set page timeout (default: 30 seconds):
nuclei -t headless.yaml -u https://example.com -page-timeout 60
Show browser
Show browser GUI for debugging:
nuclei -t headless.yaml -u https://example.com -headless-show-browser
Browser arguments
Pass custom Chrome arguments:
nuclei -t headless.yaml -u https://example.com -headless-browser-args '--disable-gpu,--no-sandbox'
User agent
Set custom user agent:
nuclei -t headless.yaml -u https://example.com -H 'User-Agent: Custom/1.0'
Cookie reuse
Reuse cookies across requests:
headless:
- cookie-reuse: true
steps:
- action: navigate
args:
url: "{{BaseURL}}/login"
Best practices
Use appropriate wait actions
Always use wait actions after navigation or interactions:
- action: navigate
args:
url: "{{BaseURL}}"
- action: waitload # Wait for page to load
Use waitvisible for dynamically loaded elements:
- action: waitvisible
args:
by: selector
value: '.dynamic-content'
Use specific wait conditions instead of sleep:
# Bad
- action: sleep
args:
duration: '5s'
# Good
- action: waitvisible
args:
by: selector
value: '.element'
Use screenshots and show-browser for debugging:
nuclei -t template.yaml -u target.com -headless-show-browser -debug
Headless browser testing is resource-intensive. Use appropriate concurrency settings and timeouts to avoid performance issues.
Selectors
Nuclei supports multiple selector types:
- CSS Selectors:
div.class, #id, [attribute]
- XPath:
//div[@class='content']
- Regex: Pattern matching
# CSS Selector
- action: click
args:
by: selector
selector: 'button.submit'
# XPath
- action: click
args:
by: xpath
value: '//button[@type="submit"]'
Troubleshooting
Browser not launching
Ensure Chrome/Chromium is installed:
# Ubuntu/Debian
apt-get install chromium-browser
# macOS
brew install chromium
Element not found
Increase wait time or use waitvisible:
- action: waitvisible
args:
by: selector
value: '.element'
timeout: 10
Memory issues
Reduce concurrency and use proper cleanup:
nuclei -t headless.yaml -u targets.txt -c 10