Skip to content

feat(input): pin a target to an ip with the ip key of target lines - #7863

Draft
mazzz3r wants to merge 4 commits into
projectdiscovery:devfrom
mazzz3r:feat/target-line-ip
Draft

mazzz3r wants to merge 4 commits into
projectdiscovery:devfrom
mazzz3r:feat/target-line-ip

Conversation

@mazzz3r

@mazzz3r mazzz3r commented Oct 11, 2026 •

Copy link
Copy Markdown

Summary

Scanning a vhost on a given server, or an origin behind a CDN, needs the hostname in the Host header, SNI and template variables while the connection goes to another ip, and one host often has to be scanned on several ips. nuclei had no input for that: a target of the ip with a Host header sends no SNI and sets {{Hostname}} to the ip, and -sa, which scans the ips DNS returns, didn't reach them correctly.

A target line can now pin its target to an ip, like curl --resolve. Each target and ip pair is a separate target, so the same host can be listed with several ips, and -sa now goes through the same path. Run in a container without network, with app.test resolving to 127.0.0.1 and 127.0.0.2, a server on each whose title or banner names it, and redir.test redirecting to other.test (127.0.0.3); JSONL fields trimmed to ip and the extracted value:

Scan dev this branch
-l with two lines pinning http://app.test:18080 to .1 and .2 unknown keys ip ip=127.0.0.1 srv=127.0.0.1, ip=127.0.0.2 srv=127.0.0.2
the same lines with -mup 1 unknown keys ip both ips kept
two lines pinning app.test:18301, network template unknown keys ip ip=127.0.0.1 one, ip=127.0.0.2 two
the same lines, javascript template reading the banner with nuclei/net unknown keys ip ip=127.0.0.1 one, ip=127.0.0.2 two
-u http://app.test:18080 -sa ip=127.0.0.1 srv=127.0.0.1, ip=127.0.0.2 srv=127.0.0.1 ip=127.0.0.1 srv=127.0.0.1, ip=127.0.0.2 srv=127.0.0.2
-u app.test:18301 -sa, network template ip=127.0.0.2 two, twice ip=127.0.0.1 one, ip=127.0.0.2 two
-u http://redir.test:18201 -sa, redirect followed srv=127.0.0.1 host=other.test:18202 srv=127.0.0.3 host=other.test:18202

Depends on projectdiscovery/fastdialer#580: WithResolveOverride, and the dial cache fix without which the second ip of a host is served from the first one's cached dialer. go.mod points fastdialer at the branch of that PR, which includes v0.5.25, until a fastdialer release includes it; that replace line is the only change to go.mod.

Changes

  • The engine pins the host of every target with a CustomIP, from a target line or -sa, with fastdialer.WithResolveOverride on the context each template runs with (executeTemplateOnInput). Every protocol that dials through fastdialer connects to the ip: HTTP, network, websocket and javascript. Javascript library calls get it through the template's context, which they receive since cancel javascript library calls with the template execution timeout #7822. Before, only HTTP used the ip, through the fastdialer.IP value, which also pinned redirects to other hosts and the dial to an HTTP proxy. HTTP keeps a client per pinned ip, so a keep-alive connection to one ip never serves a request pinned to another. Network and websocket results report the pinned ip, and scheme-less targets are probed through it.
  • Target lines accept ip. It is validated and canonicalized (::ffff:192.0.2.10 becomes 192.0.2.10; zones and unspecified addresses are rejected), and the target must have a hostname: IP, CIDR and ASN targets are rejected. The registry keys targets by input and ip, so each pair keeps its own template selection. -sa and -iv don't resolve pinned targets. A pinned ip denied by the network policy (-eh, -lna) is skipped with a warning. Pinned targets are refused with -proxy, since the proxy resolves the host itself, and with -automatic-scan, which drops the ip. pkg/input/README.md documents the key.
  • A target line without template keys no longer counts as a selection: {"target": ...} alone runs every template as before, but it no longer turns on per-target profiles or blocks -automatic-scan.
  • The host errors cache keys a failed dial of a pinned target by its ip. fastdialer reports the address with the hostname in ASCII form, so pinnedAddress compares both in ASCII form. Before, failures of pinned targets were recorded under host:port, and an unresponsive pinned ip was never skipped.
  • -mup counts the urls of each ip apart, with Fingerprinter.AcceptOn. Before, a url listed with several ips, or resolved to several with -sa, was scanned on its first ip only under -mup 1; the -sa case happens on dev too.
  • targetprofile.ParseLine returns a Line, and Bind/Merge take a *contextargs.MetaInput; nuclei is their only caller.

Headless templates and unsafe: true raw requests don't use the ip, and the README says so.

Testing

  • targetprofile: ip parsing and canonicalization, each rejected form, an ip-only line leaving the scan unscoped, and two ips of one host keeping separate selections.
  • list: pinned lines with -sa on (no DNS lookup), deduplication of equivalent ips, a pinned ip excluded by -eh, and a url pinned to two ips under -mup 1; the -sa test also checks that -mup 1 keeps a url on both resolved ips. normalize: AcceptOn. The -mup tests fail without the fix.
  • contextargs: MetaInput.Hostname, and WithCustomIP reaching a local listener for a host that doesn't resolve. hosterrorscache: the pinned key mapping, including an IDN host, which fails with the old comparison.
  • Integration: protocols/http/target-line-ip.yaml pins pinned.test to 127.0.0.1 and, where it can be bound, 127.0.0.2, and checks that each server answers with Host: pinned.test. TestNetwork/PinnedTargetLine runs network/basic.yaml on a pinned pinned.test:port. protocols/javascript/target-line-ip.yaml reads a banner over nuclei/net from both ips of a pinned pinned.test and checks the ip of each result. Before the rebase onto cancel javascript library calls with the template execution timeout #7822 it got no result: the library call didn't get the template's context, so it dialed pinned.test, which doesn't resolve.
  • go test -race passes for pkg/input/..., pkg/core, pkg/protocols/common/contextargs, pkg/protocols/common/hosterrorscache, pkg/protocols/network and internal/runner, and go test ./pkg/protocols/http/ passes. In a golang:1.27.1 container without network the same runs pass except the interactsh tests of network and http and TestReqURLPattern, which fail the same way on dev there.
  • go vet -tags=integration ./... is clean; golangci-lint run on the changed packages reports nothing in the changed files; the two issues it reports in internal/tests/integration (exporters_test.go, runner_test.go) are on dev as well.

Feature request: https://github.com/orgs/projectdiscovery/discussions/7862

Checklist

  • 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)

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Scanning a vhost on a given server, or an origin behind a CDN, needs the
hostname in the Host header, SNI and template variables while the
connection goes to another ip, and one host often has to be scanned on
several ips. -sa scans the ips DNS returns, and a target of the ip with
a Host header sends no SNI and sets {{Hostname}} to the ip.

A target line can now pin its target to an ip, like curl --resolve:

  {"target": "https://app.example", "ip": "192.0.2.10"}

Each target and ip pair is a separate target with its own template
selection. The ip is validated and canonicalized, the target must have
a hostname, -sa and -iv don't resolve a pinned target, and an ip the
network policy denies is skipped. Pinned targets are refused with
-proxy and -automatic-scan.

The engine pins the host of every target with a CustomIP, from a target
line or -sa, with fastdialer.WithResolveOverride on the context each
template runs with. HTTP, network, websocket and javascript templates
then connect to the ip, while redirects to other hosts and the proxy
dial are no longer pinned. HTTP keeps a client per pinned ip, network
and websocket results report the ip, and scheme-less targets are probed
through it.

The host errors cache keys a failed dial of a pinned target by its ip,
also for IDN hosts, and -mup counts the urls of each ip apart, so a url
listed with several ips, or resolved to several with -sa, is kept on
each of them.

go.mod points fastdialer at a fork commit with WithResolveOverride until
a fastdialer release includes it.
@mazzz3r
mazzz3r force-pushed the feat/target-line-ip branch from 1771d42 to d21749b Compare October 12, 2026 17:02
go.mod takes dev's versions, including fastdialer v0.5.25, and keeps
fastdialer replaced by the branch of projectdiscovery/fastdialer#580,
which now includes v0.5.25.
go.mod takes dev's versions, including fastdialer v0.5.26, and keeps the
fastdialer replace.
fastdialer#580 is merged into main, so the fork branch it was on no longer
exists. The replace resolves the same commit through the upstream module
path github.com/projectdiscovery/fastdialer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant