Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions pkg/protocols/headless/engine/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/url"
"time"

"golang.org/x/net/http2"
"golang.org/x/net/proxy"

"github.com/projectdiscovery/fastdialer/fastdialer/ja3/impersonate"
Expand Down Expand Up @@ -67,7 +68,7 @@ func newHttpClient(options *types.Options) (*http.Client, error) {
} else if options.AliveSocksProxy != "" {
socksURL, proxyErr := url.Parse(options.AliveSocksProxy)
if proxyErr != nil {
return nil, err
return nil, proxyErr
}
dialer, err := proxy.FromURL(socksURL, proxy.Direct)
if err != nil {
Expand All @@ -80,10 +81,28 @@ func newHttpClient(options *types.Options) (*http.Client, error) {
transport.DialContext = dc.DialContext
}

var roundTripper http.RoundTripper = transport

// TLS impersonation via utls advertises h2 in ALPN to mimic a real browser.
// Since utls returns *utls.UConn (not *tls.Conn), Go's http.Transport can't
// detect the negotiated protocol and tries HTTP/1.x on an h2 connection,
// causing "malformed HTTP response" errors. We use http2.Transport directly
// with utls dialing to properly handle h2 connections (see #6360).
if options.TlsImpersonate {
roundTripper = &http2.Transport{
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
return dialers.Fastdialer.DialTLSWithConfigImpersonate(ctx, network, addr, tlsConfig, impersonate.Random, nil)
},
TLSClientConfig: tlsConfig,
AllowHTTP: false,
DisableCompression: false,
}
}
Comment on lines +84 to +100
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's existing proxy handling with http2.Transport elsewhere in the codebase
echo "=== Searching for http2.Transport proxy handling patterns ==="
rg -n -C3 'http2\.Transport' --type=go

echo ""
echo "=== Check how fastdialer handles proxy with TLS impersonation ==="
ast-grep --pattern $'DialTLSWithConfigImpersonate($$$)'

Repository: projectdiscovery/nuclei

Length of output: 1340


🏁 Script executed:

#!/bin/bash
# Read the full context around lines 64-100 in http_client.go
echo "=== Full context of http_client.go lines 50-105 ==="
sed -n '50,105p' pkg/protocols/headless/engine/http_client.go | cat -n

echo ""
echo "=== Search for proxy configuration setup ==="
rg -n "Proxy|proxy|AliveHttpProxy|AliveSocksProxy" pkg/protocols/headless/engine/http_client.go -B2 -A2

echo ""
echo "=== Locate Fastdialer and DialTLSWithConfigImpersonate ==="
fd -name "*.go" -type f | xargs grep -l "DialTLSWithConfigImpersonate"

Repository: projectdiscovery/nuclei

Length of output: 3519


🏁 Script executed:

#!/bin/bash
# Find Fastdialer implementation
echo "=== Locating Fastdialer DialTLSWithConfigImpersonate ==="
fd "fastdialer" -type d

echo ""
echo "=== Search in common vendor/import paths ==="
fd -name "*.go" -type f 2>/dev/null | head -20 | xargs grep -l "DialTLSWithConfigImpersonate" 2>/dev/null | head -5

echo ""
echo "=== Alternative: check imports in http_client.go ==="
head -30 pkg/protocols/headless/engine/http_client.go | grep -E "import|dialers"

echo ""
echo "=== Check if fastdialer is external dependency ==="
if [ -f go.mod ]; then
  grep -i fastdialer go.mod | head -5
fi

Repository: projectdiscovery/nuclei

Length of output: 684


🏁 Script executed:

#!/bin/bash
# Verify http2.Transport capabilities and proxy handling
echo "=== Check golang.org/x/net/http2 import and usage ==="
grep -n "http2" pkg/protocols/headless/engine/http_client.go | head -10

echo ""
echo "=== Verify that http2.Transport is indeed without Proxy field ==="
# Check if anyone in codebase tries to set Proxy on http2.Transport
rg "http2\.Transport\s*\{" -A 15 pkg/

echo ""
echo "=== Check how this file uses roundTripper ==="
grep -n "roundTripper" pkg/protocols/headless/engine/http_client.go

Repository: projectdiscovery/nuclei

Length of output: 1748


Proxy configuration is lost when TLS impersonation is enabled.

When TlsImpersonate is true, the roundTripper is replaced with a new http2.Transport that doesn't incorporate any proxy settings configured on lines 64-82. If a user enables both -tlsi and a proxy (AliveHttpProxy or AliveSocksProxy), the proxy will be silently ignored.

The http2.Transport doesn't have a direct Proxy field. You need to integrate the proxy dialer into the DialTLSContext function for http2.Transport to preserve proxy support when TLS impersonation is enabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/protocols/headless/engine/http_client.go` around lines 84 - 100, The new
http2.Transport assigned to roundTripper when options.TlsImpersonate is true
ignores any proxy settings (AliveHttpProxy/AliveSocksProxy) set earlier; update
the DialTLSContext used in that http2.Transport to use the same proxy-aware
dialing logic as the original transport (i.e., route through the configured
HTTP/SOCKS proxy) instead of calling
dialers.Fastdialer.DialTLSWithConfigImpersonate directly. Concretely, modify the
DialTLSContext implementation in the http2.Transport block to detect and apply
the configured proxy (AliveHttpProxy/AliveSocksProxy) and perform TLS
impersonated dialing through that proxy (leveraging the existing proxy dialer
code path used by the original transport), ensuring TLSClientConfig (tlsConfig)
and impersonate.Random are preserved.


jar, _ := cookiejar.New(nil)

httpclient := &http.Client{
Transport: transport,
Transport: roundTripper,
Timeout: time.Duration(options.Timeout*3) * time.Second,
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
Expand All @@ -94,3 +113,4 @@ func newHttpClient(options *types.Options) (*http.Client, error) {

return httpclient, nil
}

5 changes: 4 additions & 1 deletion pkg/protocols/headless/engine/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/proto"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
)

Expand Down Expand Up @@ -48,7 +49,9 @@ func (p *Page) routingRuleHandler(httpClient *http.Client) func(ctx *rod.Hijack)
}

// perform the request
_ = ctx.LoadResponse(httpClient, true)
if err := ctx.LoadResponse(httpClient, true); err != nil {
gologger.Verbose().Msgf("headless: failed to load response for %s: %s", ctx.Request.URL(), err)
}

if !p.options.DisableCookie {
// retrieve the updated cookies from the native http client and inject them into the shared cookie jar
Expand Down
Loading