feat(headless): add `cdp-endpoint` option
Proposed changes
Close #5692
How has this been tested?
# tty1
/path/to/chrome --headless --remote-debugging-port=9222 --ignore-certificate-errors --ignore-ssl-errors
# tty2
go run cmd/nuclei/main.go -headless -t headless-template-1.yaml -u http://scanme.sh -cdp-endpoint "CHROME_DEVTOOLS_ENDPOINT_URL"
Checklist
- [x] Pull request is created against the dev branch
- [ ] All checks passed (lint, unit/integration/regression tests etc.) with my changes
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added necessary documentation (if appropriate)
Summary by CodeRabbit
-
New Features
- Added a CLI option to connect to a remote browser via a Chrome DevTools Protocol (CDP) endpoint.
-
Refactor
- Initialization and shutdown now distinguish local vs. remote browser usage, skipping local launch and cleanup when a remote CDP endpoint is used.
-
Documentation
- Updated HEADLESS docs across locales to document the new CDP endpoint option.
But I’m unable to connect to the CDP URL provided by finic:
[FTL] Could not create runner: websocket bad handshake: 400 Bad Request. Failed to open a WebSocket connection: invalid Sec-WebSocket-Key header: nil; Incorrect padding.
Not sure if this is an issue with go-rod or with finic. It needs further investigation, or maybe even a hacky workaround. I’ll try testing it via Playwright - if the issue can’t be reproduced there, I’ll go ahead and raise it as an issue in go-rod.
@ehsandeep - If we look at the trace, it doesn't seem related to our source code - the underlying issue is in go-rod. Also, this panic wouldn't happen if we were using the system CDP.
$ go run cmd/nuclei/main.go -u http://scanme.sh -cdp-endpoint "ws://*********:9222/devtools/browser/************************************" -headless -pt headless
__ _
____ __ _______/ /__ (_)
/ __ \/ / / / ___/ / _ \/ /
/ / / / /_/ / /__/ / __/ /
/_/ /_/\__,_/\___/_/\___/_/ v3.3.5
projectdiscovery.io
[INF] Current nuclei version: v3.3.5 (latest)
[INF] Current nuclei-templates version: v10.0.3 (latest)
[WRN] Scan results upload to cloud is disabled.
[INF] New templates added in latest release: 116
[INF] Templates loaded for current scan: 18
[INF] Executing 18 signed templates from projectdiscovery/nuclei-templates
[INF] Targets loaded for current scan: 1
[piratebay] [headless] [info] https://thepiratebay.org/search.php?q=user:{{user}}
[extract-urls] [headless] [info] http://scanme.sh [""]
We either need to tweak the Browserless container or there's something on our end that needs to be handled - but we need to figure out the root cause (template) that's triggering that panic.
This pull request has been automatically marked as stale due to inactivity. It will be closed in 7 days if no further activity occurs. Please update if you wish to keep it open.
Walkthrough
A new cdp-endpoint CLI flag and CDPEndpoint option were added. The headless browser engine now conditionally uses the provided CDP WebSocket URL instead of launching a local Chrome instance; initialization and cleanup are skipped when connecting to a remote CDP endpoint.
Changes
| Cohort / File(s) | Change Summary |
|---|---|
CLI flag cmd/nuclei/main.go |
Added cdp-endpoint (cdpe) CLI flag bound to options.CDPEndpoint. |
Options struct pkg/types/types.go |
Added CDPEndpoint string field to Options. |
Headless engine pkg/protocols/headless/engine/engine.go |
Refactored New and Close to: when CDPEndpoint is set, use it as the launcher URL and skip local Chrome launch, temp dir creation, musl detection, launcher setup, and local cleanup; otherwise preserve existing local launch and cleanup logic. Proxy and extra args apply only to local launches. |
Documentation README.md, README_CN.md, README_ES.md, README_ID.md, README_KR.md, README_PT-BR.md |
Added or adjusted HEADLESS section entries to document -cdpe, -cdp-endpoint flag (formatting/content updates across locales). |
Sequence Diagram(s)
sequenceDiagram
participant User
participant CLI
participant Engine
participant Browser
User->>CLI: nuclei --cdp-endpoint ws://host:port
CLI->>Engine: Pass Options (CDPEndpoint set)
alt CDP endpoint provided
Engine->>Browser: Connect to provided CDP WebSocket URL
Note right of Engine #f9f0c1: Skip temp dir creation\nSkip launching local Chrome
User->>Engine: Perform headless actions
Engine->>Browser: Send CDP commands over WebSocket
User->>Engine: Close()
Engine-->>Browser: Do not terminate remote browser (leave running)
else No CDP endpoint
Engine->>Engine: Create temp dir & configure launcher
Engine->>Browser: Launch local Chrome process
User->>Engine: Perform headless actions
Engine->>Browser: Send CDP commands
User->>Engine: Close()
Engine->>Browser: Terminate Chrome process and cleanup
end
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~20 minutes
Assessment against linked issues
| Objective (issue #5692) | Addressed | Explanation |
|---|---|---|
Add cdp-endpoint flag to accept a WebSocket/CDP URL for headless mode |
✅ | Flag and Options field added. |
| Use provided CDP WebSocket URL to connect to existing/remote browser | ✅ | Engine uses provided URL instead of launching Chrome. |
| Allow users to keep remote browser lifecycle external (skip terminating remote) | ✅ | Close() skips terminating remote browser when CDP endpoint used. |
| Remove chromium dependency / make base image scratch (optional) | ❌ | Local launch path and related launcher code remain; chromium removal not addressed. |
Poem
I’m a rabbit by the CDP shore,
I hop to ws:// and ask for more. 🐇
No local launch, just websockets spun,
Commands go out — the tabs have fun.
I close my paws, the remote stays on.
Pre-merge checks and finishing touches
✅ Passed checks (5 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | The title "feat(headless): add cdp-endpoint option" succinctly and accurately describes the main change of adding the CDP endpoint flag to the headless mode, following conventional commit conventions and clearly indicating the feature scope. |
| Linked Issues Check | ✅ Passed | The changes add the CDPEndpoint field to the Options struct, bind it as a CLI flag, conditionally use the provided WebSocket URL in the headless engine initialization and cleanup logic, and update documentation accordingly, thereby fulfilling the objectives of issue #5692 to enable remote CDP endpoint connections in headless mode. |
| Out of Scope Changes Check | ✅ Passed | All code and documentation changes directly relate to introducing and supporting the cdp-endpoint option and its conditional handling in the headless engine, with no modifications detected outside the scope of the linked feature request. |
| Docstring Coverage | ✅ Passed | No functions found in the changes. Docstring coverage check skipped. |
✨ Finishing touches
🧪 Generate unit tests (beta)
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
- [ ] Commit unit tests in branch
dwisiswant0/feat/headless/cdp-endpoint-option
📜 Recent review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📥 Commits
Reviewing files that changed from the base of the PR and between 9fef94adc52f1c536eada11efa042367a3597eed and 0c8fb50b7d1ab5b50c6f3add2ea6491eae321de9.
📒 Files selected for processing (6)
-
README.md(1 hunks) -
README_CN.md(1 hunks) -
README_ES.md(1 hunks) -
README_ID.md(1 hunks) -
README_KR.md(1 hunks) -
README_PT-BR.md(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- README_PT-BR.md
- README.md
Comment @coderabbitai help to get the list of available commands and usage tips.
https://github.com/orgs/projectdiscovery/discussions/6453
Very cool and useful functionality, I hope it'll become part of upstream. The only missing thing here is no incognito mode which could be highly useful. Currently upstream has the following from (called from executeRequestWithPayloads)
func (b *Browser) NewInstance() (*Instance, error) { browser, err := b.engine.Incognito() if err != nil { return nil, err }
and it's not configurable. If we switch to something like
func (b *Browser) NewInstance() (*Instance, error) { var browser *rod.Browser if b.options.HeadlessNoIncognito { // Use the existing browser directly instead of creating an incognito instance browser = b.engine } else { var err error browser, err = b.engine.Incognito() if err != nil { return nil, err } }
we could get much more flexible result. Of course it can be added separetely. Correct me if I'm wrong :-)
[...] The only missing thing here is no incognito mode which could be highly useful. [...]
Since we’re connecting via Chrome DevTools Proto URL (browser’s already live), flags don’t take effect after launch [and are not controllable from the source], right? They’re static, hence any flag changes won’t apply.
I’m not 100% certain. AFK rn, will PoC later to confirm.
Since we’re connecting via Chrome DevTools Proto URL (browser’s already live), flags don’t take effect after launch [and are not controllable from the source], right? They’re static, hence any flag changes won’t apply.
I’m not 100% certain. AFK rn, will PoC later to confirm.
I was talking about request.go#L115 - as I see for such requests we create a new instance of browser based on existing one instance.go#L30 no matter if it's provided CDP or not - correct me if I'm wrong. If we want to use the same instance a minor modification is required like I mentioned before - something like avoid of browser, err = b.engine.Incognito(). For production usecase I added this modification and the result works like a charm :-)