Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ endif

.PHONY: all build build-stats clean devtools-all devtools-bindgen devtools-scrapefuncs fuzz fuzz-ci fuzz-tools
.PHONY: devtools-tsgen docs docgen dsl-docs functional go-build lint lint-strict fuzzplayground syntax-docs
.PHONY: integration integration-debug regression jsupdate-all jsupdate-bindgen jsupdate-tsgen memogen scan-charts test test-with-lint
.PHONY: integration integration-debug regression jsupdate-all jsupdate-bindgen jsupdate-tsgen memogen scan-charts test test-with-lint test-sandbox-linux
.PHONY: tidy ts verify download vet template-validate build-fuzz discover-fuzz-packages

all: build
Expand Down Expand Up @@ -96,6 +96,11 @@ integration:
integration-debug:
$(GOTEST) -tags=integration ./internal/tests/integration -v $(GO_TEST_ARGS) -args $(INTEGRATION_ARGS)

# Runs Landlock and bubblewrap sandbox tests inside Linux Docker (macOS/Windows hosts).
test-sandbox-linux:
docker run --rm -e GOTOOLCHAIN=auto -v "$(PWD):/src" -w /src golang:bookworm \
bash -c 'apt-get update -qq && apt-get install -y -qq bubblewrap >/dev/null && go test ./pkg/protocols/common/sandbox/... ./pkg/protocols/code/... -count=1 -run "Landlock|Apply|Supported|Bubblewrap|DisableSandbox"'

# Opt-in HTTP engine scale regression harness (not part of CI). Stands up many
# loopback hosts and asserts finding parity across a diverse template set.
# Override host count with NUCLEI_SCALE_HOSTS, e.g. NUCLEI_SCALE_HOSTS=500 make regression
Expand Down
4 changes: 3 additions & 1 deletion cmd/nuclei/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,9 @@ on extensive configurability, massive extensibility and ease of use.`)
flagSet.BoolVar(&options.ZTLS, "ztls", false, "use ztls library with autofallback to standard one for tls13 [Deprecated] autofallback to ztls is enabled by default"), //nolint:all
flagSet.StringVar(&options.SNI, "sni", "", "tls sni hostname to use (default: input domain name)"),
flagSet.DurationVarP(&options.DialerKeepAlive, "dialer-keep-alive", "dka", 0, "keep-alive duration for network requests."),
flagSet.BoolVarP(&options.AllowLocalFileAccess, "allow-local-file-access", "lfa", false, "allows file (payload) access anywhere on the system"),
flagSet.BoolVarP(&options.AllowLocalFileAccess, "allow-local-file-access", "lfa", false, "allows local file access from additional allowed directories (cwd and --allowed-paths)"),
flagSet.StringSliceVarP(&options.AllowedPaths, "allowed-paths", "ap", nil, "additional filesystem roots allowed with -lfa (repeatable)", goflags.CommaSeparatedStringSliceOptions),
flagSet.BoolVar(&options.DisableSandbox, "no-sandbox", false, "disable OS-level filesystem sandbox enforcement"),
flagSet.BoolVarP(&options.RestrictLocalNetworkAccess, "restrict-local-network-access", "lna", false, "blocks connections to the local / private network"),
flagSet.StringVarP(&options.Interface, "interface", "i", "", "network interface to use for network scan"),
flagSet.StringVarP(&options.AttackType, "attack-type", "at", "", "type of payload combinations to perform (batteringram,pitchfork,clusterbomb)"),
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ require (
github.com/jcmturner/gokrb5/v8 v8.4.4
github.com/jhump/protoreflect v1.17.0
github.com/kitabisa/go-ci v1.0.3
github.com/landlock-lsm/go-landlock v0.9.0
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa
github.com/lib/pq v1.12.3
github.com/logrusorgru/aurora/v4 v4.0.0
Expand All @@ -100,7 +101,7 @@ require (
github.com/projectdiscovery/gologger v1.1.71
github.com/projectdiscovery/gostruct v0.0.2
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76
github.com/projectdiscovery/gozero v0.1.1-0.20260705152723-8e53eced4d92
github.com/projectdiscovery/httpx v1.10.0
github.com/projectdiscovery/mapcidr v1.1.97
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5
Expand Down Expand Up @@ -390,6 +391,7 @@ require (
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/arch v0.3.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
mellium.im/sasl v0.3.2 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.2 // indirect
)
Expand Down
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/landlock-lsm/go-landlock v0.9.0 h1:2q8G8yx9Hsd5bV+R6PJfgQl0zszNxC8KO+SIqGwfxlw=
github.com/landlock-lsm/go-landlock v0.9.0/go.mod h1:mn5GSi81Jf7yMs5WSi+SUi4sUeNLUGVdbT4Id6wXNQw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
Expand Down Expand Up @@ -861,8 +863,8 @@ github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBD
github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE=
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e h1:vxzgQlz2Cy/YvizYDQx9OhucBcmBotfDhbQ4yCY2vfA=
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e/go.mod h1:xH7bPwHxUlz1yx9UlVeTF+UVCUaKhTnZgaxHb5z362E=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 h1:AN70bbi6BBs7KpIM9w0LxygUN7uzT/oH+owDIQ+Fz/k=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76/go.mod h1:cWHYnRXoYWHtTpOYyAp5laGYX8GH8ITUhgQaP8G/8FA=
github.com/projectdiscovery/gozero v0.1.1-0.20260705152723-8e53eced4d92 h1:l7TNT/4nh8NCDaIuywPlKjmxNAH/ovWylhMQf9VZTYY=
github.com/projectdiscovery/gozero v0.1.1-0.20260705152723-8e53eced4d92/go.mod h1:cWHYnRXoYWHtTpOYyAp5laGYX8GH8ITUhgQaP8G/8FA=
github.com/projectdiscovery/hmap v0.0.101 h1:zXM6YtLmsn8Q0CUUw8QavhqWmiQYwaw+/U679Rr00pc=
github.com/projectdiscovery/hmap v0.0.101/go.mod h1:w6N9/a5H8kvyx53AhtPDUWe5Qq3D6NBDPA23glHpa/Q=
github.com/projectdiscovery/httpx v1.10.0 h1:sZvfeJDeX4eJfB5Oh29My4gOHmLfvpCt2wCqhargJrE=
Expand Down Expand Up @@ -1625,6 +1627,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY=
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
Expand Down
1 change: 1 addition & 0 deletions internal/tests/integration/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func integrationFamilies() []integrationFamily {
{Name: "generic", Cases: genericTestcases},
{Name: "dsl", Cases: dslTestcases},
{Name: "javascript", Cases: jsTestcases},
{Name: "security-hardening", Cases: securityHardeningTestcases},
{Name: "matcher-status", Cases: matcherStatusTestcases},
{Name: "exporters", Cases: exportersTestCases},
{Name: "profile-loader", Cases: profileLoaderTestcases},
Expand Down
84 changes: 84 additions & 0 deletions internal/tests/integration/security_hardening_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//go:build integration
// +build integration

package integration_test

import (
"os"
"path/filepath"

"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
)

var securityHardeningTestcases = []integrationCase{
{Path: "protocols/javascript/fs-read-deny.yaml", TestCase: &javascriptFSReadDeny{}},
{Path: "protocols/javascript/fs-read-deny-lfa.yaml", TestCase: &javascriptFSReadDenyWithLFA{}},
{Path: "protocols/javascript/net-deny-excluded.yaml", TestCase: &javascriptNetDenyExcluded{}},
{Path: "protocols/javascript/fs-read-allowed-paths.yaml", TestCase: &javascriptFSReadAllowedPaths{}},
}

func securityHardeningRunner(allowLFA bool) *testutils.Runner {
return suite.runner.Clone(testutils.WithAllowLocalFileAccess(allowLFA))
}

func runSecurityTemplate(filePath string, allowLFA bool, debug bool, extra ...string) ([]string, error) {
return securityHardeningRunner(allowLFA).TemplateResults(filePath, "127.0.0.1", debug, extra...)
}

type javascriptFSReadDeny struct{}

func (j *javascriptFSReadDeny) Execute(filePath string) error {
results, err := runSecurityTemplate(filePath, false, debug)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}

type javascriptFSReadDenyWithLFA struct{}

func (j *javascriptFSReadDenyWithLFA) Execute(filePath string) error {
results, err := runSecurityTemplate(filePath, true, debug, "-allow-local-file-access")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}

type javascriptNetDenyExcluded struct{}

func (j *javascriptNetDenyExcluded) Execute(filePath string) error {
results, err := runSecurityTemplate(filePath, false, debug, "-eh", "203.0.113.10")
if err != nil {
return err
}
return expectResultsCount(results, 1)
}

type javascriptFSReadAllowedPaths struct{}

func (j *javascriptFSReadAllowedPaths) Execute(filePath string) error {
grantedDir, err := os.MkdirTemp("", "nuclei-allowed-paths-*")
if err != nil {
return err
}
defer os.RemoveAll(grantedDir)

secretPath := filepath.Join(grantedDir, "secret.txt")
if err := os.WriteFile(secretPath, []byte("granted-secret"), 0o600); err != nil {
return err
}

results, err := runSecurityTemplate(
filePath,
true,
debug,
"-allow-local-file-access",
"-ap", grantedDir,
"-var", "ReadPath="+secretPath,
)
if err != nil {
return err
}
return expectResultsCount(results, 1)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
id: javascript-fs-read-allowed-paths

info:
name: JavaScript FS Read Allowed Paths
author: pdteam
severity: info
description: reads a file granted via --allowed-paths

javascript:
- code: |
const fs = require('nuclei/fs');
let ok = false;
let content = "";
try {
content = fs.ReadFileAsString(ReadPath);
ok = content === "granted-secret";
} catch (e) {
ok = false;
}
Export(to_json({ ok: ok, content: content }));
args:
Host: "{{Host}}"
ReadPath: "{{ReadPath}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- success == true
- type: word
part: response
condition: and
words:
- '"ok": true'
- "granted-secret"
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
id: javascript-fs-read-deny-lfa

info:
name: JavaScript FS Read Deny With LFA
author: pdteam
severity: info
description: confirms -lfa does not bypass absolute paths outside the allowlist

javascript:
- code: |
const fs = require('nuclei/fs');
let ok = true;
let errMsg = "";
try {
fs.ReadFileAsString('/etc/passwd');
} catch (e) {
ok = false;
errMsg = String(e);
}
Export(to_json({ ok: ok, err: errMsg }));
args:
Host: "{{Host}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- success == true
- type: word
part: response
condition: and
words:
- '"ok": false'
- "outside allowed directories"
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
id: javascript-fs-read-deny

info:
name: JavaScript FS Read Deny
author: pdteam
severity: info
description: attempts to read a file outside the sandbox allowlist

javascript:
- code: |
const fs = require('nuclei/fs');
let ok = true;
let errMsg = "";
try {
fs.ReadFileAsString('/etc/passwd');
} catch (e) {
ok = false;
errMsg = String(e);
}
Export(to_json({ ok: ok, err: errMsg }));
args:
Host: "{{Host}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- success == true
- type: word
part: response
words:
- '"ok": false'
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
id: javascript-net-deny-excluded

info:
name: JavaScript Net Deny Excluded Host
author: pdteam
severity: info
description: attempts to dial a host blocked by network policy

javascript:
- code: |
const net = require('nuclei/net');
let ok = true;
let errMsg = "";
try {
net.Open('tcp', '203.0.113.10:445');
} catch (e) {
ok = false;
errMsg = String(e);
}
Export(to_json({ ok: ok, err: errMsg }));
args:
Host: "{{Host}}"
matchers-condition: and
matchers:
- type: dsl
dsl:
- success == true
- type: word
part: response
condition: and
words:
- '"ok": false'
- "denied address found"
6 changes: 6 additions & 0 deletions internal/tests/testutils/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ func WithExtraArgs(args ...string) RunnerOption {
}
}

func WithAllowLocalFileAccess(allow bool) RunnerOption {
return func(r *Runner) {
r.AllowLocalFileAccess = allow
}
}

var defaultRunner = NewRunner()

func DefaultRunner() *Runner {
Expand Down
27 changes: 23 additions & 4 deletions pkg/js/compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,27 @@ func TestNewCompilerConsoleDebug(t *testing.T) {
}
}

func outsideModulePath(t *testing.T, name string) string {
t.Helper()
// os.UserHomeDir (not os.Getenv("HOME")) so this resolves to an absolute,
// writable location outside the templates allowlist on every OS. HOME is
// unset on GitHub Actions windows-latest, which would otherwise collapse to
// a relative path resolved under the templates dir and void the denial test.
home, err := os.UserHomeDir()
require.NoError(t, err)
outsideDir := filepath.Join(home, ".nuclei-test-outside-"+t.Name())
require.NoError(t, os.MkdirAll(outsideDir, 0o700))
t.Cleanup(func() { _ = os.RemoveAll(outsideDir) })
return writeModuleFile(t, outsideDir, name, `module.exports = { value: "outside-secret" };`)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestRequireLocalFileAccessDenied(t *testing.T) {
modulePath := writeModuleFile(t, t.TempDir(), "outside.js", `module.exports = { value: "outside-secret" };`)
modulePath := outsideModulePath(t, "outside.js")
script := fmt.Sprintf(`var helper = require(%q); ExportAs("value", helper.value); true;`, modulePath)

result, err := executeScript(t, t.Name(), false, script)
require.Error(t, err)
require.Contains(t, err.Error(), "-lfa is not enabled")
require.Contains(t, err.Error(), "outside")
require.Equal(t, err.Error(), result["error"])
}

Expand Down Expand Up @@ -83,7 +97,12 @@ func TestRequireLocalFileAccessAllowed(t *testing.T) {
}

func TestRequireDoesNotReusePrivilegedModuleCacheAcrossExecutions(t *testing.T) {
modulePath := writeModuleFile(t, t.TempDir(), "outside.js", `module.exports = { value: "outside-ok" };`)
cwd, err := os.Getwd()
require.NoError(t, err)
moduleDir := filepath.Join(cwd, ".nuclei-module-cache-test-"+t.Name())
require.NoError(t, os.MkdirAll(moduleDir, 0o700))
t.Cleanup(func() { _ = os.RemoveAll(moduleDir) })
modulePath := writeModuleFile(t, moduleDir, "outside.js", `module.exports = { value: "outside-ok" };`)
program, err := goja.Compile("", fmt.Sprintf(`require(%q).value`, modulePath), false)
require.NoError(t, err)

Expand All @@ -103,7 +122,7 @@ func TestRequireDoesNotReusePrivilegedModuleCacheAcrossExecutions(t *testing.T)
ExecutionId: denyExecutionID,
}, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "-lfa is not enabled")
require.Contains(t, err.Error(), "outside")
}

func TestExecuteWithRuntimeCleansUpAfterCallbackPanic(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/js/libs/dcerpc/dcerpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func (c *Client) RpcDump(ctx context.Context) ([]Endpoint, error) {
if dialer == nil {
return nil, fmt.Errorf("dialers not initialized for execution %s", c.nj.ExecutionId())
}
conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(c.Host, strconv.Itoa(135)))
conn, err := protocolstate.DialAllowedWithExecutionID(ctx, c.nj.ExecutionId(), "tcp", net.JoinHostPort(c.Host, strconv.Itoa(135)))
if err != nil {
return nil, fmt.Errorf("dial epmapper: %w", err)
}
Expand Down
Loading
Loading