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