mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-07 09:46:03 -10:00
fix(js/net): enforce network policy on proxied target in JS net dial
When an HTTP(S) proxy is configured, the nuclei/net JS library dialed the proxy through Fastdialer (which validates the proxy host) but tunneled the connection target inside an HTTP CONNECT without validating it. This let a template reach a deny-listed destination (-exclude-hosts / -restrict-local-network-access) that a direct dial would have blocked, bypassing the network policy. Validate the target host against the network policy in the proxy branch of Dial() before contacting the proxy, matching the guard the direct path gets from Fastdialer. Adds a regression test asserting the proxy is never contacted for a deny-listed target. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
6a42a7bf52
commit
1c0f77d28e
2 files changed
+52
-5
No files matched your search
@@ -167,6 +167,14 @@ func Dial(ctx context.Context, protocol, address string) (net.Conn, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid proxy URL %s: %w", redactProxyURL(proxyURL), err)
|
||||
}
|
||||
// Enforce the network policy on the tunneled target. Direct dials go
|
||||
// through Fastdialer, which validates the destination itself, but the
|
||||
// proxy path only dials the proxy through Fastdialer and tunnels the
|
||||
// target inside an HTTP CONNECT, so the target would otherwise skip the
|
||||
// deny-list (-exclude-hosts / -restrict-local-network-access) checks.
|
||||
if !protocolstate.IsHostAllowed(executionId, address) {
|
||||
return nil, fmt.Errorf("host %s is not allowed by network policy", address)
|
||||
}
|
||||
switch parsed.Scheme {
|
||||
case "http", "https":
|
||||
return dialHTTPProxy(ctx, dialer.Fastdialer.Dial, proxyURL, address, getTimeoutFromContext(ctx))
|
||||
|
||||
@@ -221,7 +221,7 @@ func TestDial(t *testing.T) {
|
||||
t.Cleanup(func() { protocolstate.Close(opts.ExecutionId) })
|
||||
|
||||
ctx := context.WithValue(context.Background(), "executionId", opts.ExecutionId) // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "socks5://127.0.0.1:1080") // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "socks5://127.0.0.1:1080") // nolint: staticcheck
|
||||
_, err := Dial(ctx, "tcp", "example.com:80")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unsupported proxy scheme")
|
||||
@@ -233,7 +233,7 @@ func TestDial(t *testing.T) {
|
||||
t.Cleanup(func() { protocolstate.Close(opts.ExecutionId) })
|
||||
|
||||
ctx := context.WithValue(context.Background(), "executionId", opts.ExecutionId) // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "://bad") // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "://bad") // nolint: staticcheck
|
||||
_, err := Dial(ctx, "tcp", "example.com:80")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid proxy URL")
|
||||
@@ -245,12 +245,51 @@ func TestDial(t *testing.T) {
|
||||
t.Cleanup(func() { protocolstate.Close(opts.ExecutionId) })
|
||||
|
||||
ctx := context.WithValue(context.Background(), "executionId", opts.ExecutionId) // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "http://127.0.0.1:8080") // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "http://127.0.0.1:8080") // nolint: staticcheck
|
||||
_, err := Dial(ctx, "udp", "example.com:53")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "does not support protocol")
|
||||
})
|
||||
|
||||
t.Run("proxy path enforces network policy on the target", func(t *testing.T) {
|
||||
// A proxy dial must not let a template reach a deny-listed target by
|
||||
// tunnelling it inside an HTTP CONNECT. The policy check has to run on
|
||||
// the target before the proxy is contacted.
|
||||
proxyLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = proxyLn.Close() }()
|
||||
|
||||
proxyHit := make(chan struct{}, 1)
|
||||
go func() {
|
||||
conn, err := proxyLn.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
proxyHit <- struct{}{}
|
||||
}()
|
||||
|
||||
opts := &types.Options{
|
||||
ExecutionId: "net-dial-policy-" + t.Name(),
|
||||
ExcludeTargets: []string{"127.0.0.2"},
|
||||
}
|
||||
require.NoError(t, protocolstate.Init(opts))
|
||||
t.Cleanup(func() { protocolstate.Close(opts.ExecutionId) })
|
||||
|
||||
ctx := context.WithValue(context.Background(), "executionId", opts.ExecutionId) // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "http://"+proxyLn.Addr().String()) // nolint: staticcheck
|
||||
|
||||
_, err = Dial(ctx, "tcp", "127.0.0.2:9999")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "not allowed by network policy")
|
||||
|
||||
select {
|
||||
case <-proxyHit:
|
||||
t.Fatal("proxy was contacted for a deny-listed target")
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("open through local http proxy", func(t *testing.T) {
|
||||
target, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
@@ -299,8 +338,8 @@ func TestDial(t *testing.T) {
|
||||
require.NoError(t, protocolstate.Init(opts))
|
||||
t.Cleanup(func() { protocolstate.Close(opts.ExecutionId) })
|
||||
|
||||
ctx := context.WithValue(context.Background(), "executionId", opts.ExecutionId) // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "http://"+proxyLn.Addr().String()) // nolint: staticcheck
|
||||
ctx := context.WithValue(context.Background(), "executionId", opts.ExecutionId) // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "proxyURL", "http://"+proxyLn.Addr().String()) // nolint: staticcheck
|
||||
ctx = context.WithValue(ctx, "timeoutVariants", &types.Timeouts{TcpReadTimeout: 5 * time.Second}) // nolint: staticcheck
|
||||
|
||||
nconn, err := Open(ctx, "tcp", target.Addr().String())
|
||||
|
||||
Reference in new issue
Block a user