mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 02:08:33 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bb0e39108 | ||
|
|
080c2fe402 | ||
|
|
33a9efa4b3 | ||
|
|
0a650af86e | ||
|
|
5908879cf1 | ||
|
|
bd13f61f57 | ||
|
|
c2681f596d | ||
|
|
47d22c6ae4 | ||
|
|
a2c73cbeb5 | ||
|
|
4195b6992c | ||
|
|
8e389b32d9 |
No files matched your search
@@ -51,6 +51,11 @@ func init() {
|
||||
return useragent.PickRandom().Raw, nil
|
||||
}))
|
||||
|
||||
// file() reads helper-file contents through Options.LoadHelperFile so the
|
||||
// same sandbox as payloads:/includes applies (templates dir, same-dir under
|
||||
// home, -lfa, hard-link rejection). Requires WithFileLoadContext.
|
||||
registerFileHelper()
|
||||
|
||||
dsl.PrintDebugCallback = func(args ...interface{}) error {
|
||||
gologger.Debug().Msgf("print_debug value: %s", fmt.Sprint(args...))
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/projectdiscovery/dsl"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/utils/errkit"
|
||||
unitutils "github.com/projectdiscovery/utils/unit"
|
||||
)
|
||||
|
||||
const (
|
||||
// maxFileHelperSize is the maximum number of bytes file() will read.
|
||||
// Matches the default HTTP response read limit.
|
||||
maxFileHelperSize = int64(10 * unitutils.Mega)
|
||||
)
|
||||
|
||||
// FileLoadContext carries the sandbox inputs required by the file() DSL helper.
|
||||
// It must be installed with WithFileLoadContext for the duration of template
|
||||
// evaluation on a worker goroutine.
|
||||
type FileLoadContext struct {
|
||||
Options *types.Options
|
||||
TemplatePath string
|
||||
Catalog catalog.Catalog
|
||||
|
||||
cache sync.Map // abs/logical path -> string contents
|
||||
}
|
||||
|
||||
type fileLoadFrame struct {
|
||||
ctx *FileLoadContext
|
||||
prev *fileLoadFrame
|
||||
}
|
||||
|
||||
var fileLoadByGoid sync.Map // uint64 -> *fileLoadFrame
|
||||
|
||||
// WithFileLoadContext installs ctx for the calling goroutine, runs fn, then
|
||||
// restores the previous context (if any). Nested calls are supported.
|
||||
func WithFileLoadContext(ctx *FileLoadContext, fn func()) {
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
id := goid()
|
||||
if id == 0 {
|
||||
// goid could not be parsed; never share one slot between goroutines,
|
||||
// so file() fails closed with "no template sandbox context".
|
||||
fn()
|
||||
return
|
||||
}
|
||||
var prev *fileLoadFrame
|
||||
if existing, ok := fileLoadByGoid.Load(id); ok {
|
||||
prev = existing.(*fileLoadFrame)
|
||||
}
|
||||
fileLoadByGoid.Store(id, &fileLoadFrame{ctx: ctx, prev: prev})
|
||||
defer func() {
|
||||
if prev == nil {
|
||||
fileLoadByGoid.Delete(id)
|
||||
} else {
|
||||
fileLoadByGoid.Store(id, prev)
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}
|
||||
|
||||
func currentFileLoadContext() *FileLoadContext {
|
||||
id := goid()
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
if v, ok := fileLoadByGoid.Load(id); ok {
|
||||
if frame := v.(*fileLoadFrame); frame != nil {
|
||||
return frame.ctx
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerFileHelper() {
|
||||
_ = dsl.AddFunction(dsl.NewWithMultipleSignatures("file", []string{
|
||||
"(path string) string",
|
||||
}, false, fileHelper))
|
||||
}
|
||||
|
||||
func fileHelper(args ...interface{}) (interface{}, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, dsl.ErrInvalidDslFunction
|
||||
}
|
||||
path := types.ToString(args[0])
|
||||
if path == "" {
|
||||
return nil, errkit.New("file(): path must not be empty")
|
||||
}
|
||||
|
||||
ctx := currentFileLoadContext()
|
||||
if ctx == nil || ctx.Options == nil {
|
||||
return nil, errkit.New("file(): no template sandbox context (only available during template execution)")
|
||||
}
|
||||
|
||||
if cached, ok := ctx.cache.Load(path); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
contents, err := ctx.readFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.cache.Store(path, contents)
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (ctx *FileLoadContext) readFile(path string) (string, error) {
|
||||
reader, err := ctx.Options.LoadHelperFile(path, ctx.TemplatePath, ctx.Catalog)
|
||||
if err != nil {
|
||||
return "", errkit.Wrapf(err, "file(): could not load %q", path)
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
if file, ok := reader.(*os.File); ok {
|
||||
info, statErr := file.Stat()
|
||||
if statErr != nil {
|
||||
return "", errkit.Wrapf(statErr, "file(): could not stat %q", path)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", errkit.Newf("file(): %q is not a regular file", path)
|
||||
}
|
||||
if info.Size() > maxFileHelperSize {
|
||||
return "", errkit.Newf("file(): %q exceeds maximum size of %d bytes", path, maxFileHelperSize)
|
||||
}
|
||||
}
|
||||
|
||||
limited := io.LimitReader(reader, maxFileHelperSize+1)
|
||||
data, err := io.ReadAll(limited)
|
||||
if err != nil {
|
||||
return "", errkit.Wrapf(err, "file(): could not read %q", path)
|
||||
}
|
||||
if int64(len(data)) > maxFileHelperSize {
|
||||
return "", errkit.Newf("file(): %q exceeds maximum size of %d bytes", path, maxFileHelperSize)
|
||||
}
|
||||
|
||||
// Go strings are binary-safe; keep opaque bytes for payload use.
|
||||
return string(data), nil
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
folderutil "github.com/projectdiscovery/utils/folder"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestFileHelperSandboxDeniesUntrustedPaths exercises path variants that must
|
||||
// remain unreadable under the default helper-file policy.
|
||||
func TestFileHelperSandboxDeniesUntrustedPaths(t *testing.T) {
|
||||
home, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
|
||||
outsideDir := t.TempDir()
|
||||
outsideFile := filepath.Join(outsideDir, "secret.txt")
|
||||
require.NoError(t, os.WriteFile(outsideFile, []byte("outside-secret"), 0o600))
|
||||
|
||||
homeSibling := filepath.Join(home, "sibling-secret.txt")
|
||||
require.NoError(t, os.WriteFile(homeSibling, []byte("home-sibling"), 0o600))
|
||||
|
||||
prefixSiblingDir := filepath.Join(home, "nuclei-templatesX")
|
||||
require.NoError(t, os.MkdirAll(prefixSiblingDir, 0o755))
|
||||
prefixSiblingFile := filepath.Join(prefixSiblingDir, "payload.txt")
|
||||
require.NoError(t, os.WriteFile(prefixSiblingFile, []byte("prefix-sibling"), 0o600))
|
||||
|
||||
nestedOutside := filepath.Join(home, "nested", "deep", "secret.txt")
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(nestedOutside), 0o755))
|
||||
require.NoError(t, os.WriteFile(nestedOutside, []byte("nested-secret"), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
expr string
|
||||
}{
|
||||
{name: "absolute_outside_home", expr: fileExpr(outsideFile)},
|
||||
{name: "absolute_under_home_but_outside_template_and_templates_dir", expr: fileExpr(homeSibling)},
|
||||
{name: "absolute_prefix_sibling_of_templates_dir", expr: fileExpr(prefixSiblingFile)},
|
||||
{name: "absolute_nested_under_home_outside_template_tree", expr: fileExpr(nestedOutside)},
|
||||
{name: "relative_parent", expr: `file("../sibling-secret.txt")`},
|
||||
{name: "relative_parent_chain", expr: `file("../../sibling-secret.txt")`},
|
||||
{name: "relative_dotdot_via_subdir", expr: `file("payloads/../../sibling-secret.txt")`},
|
||||
{name: "relative_dot_slash_parent", expr: `file("./../sibling-secret.txt")`},
|
||||
{name: "relative_nested_escape", expr: `file("a/b/../../../sibling-secret.txt")`},
|
||||
{name: "relative_to_nested_outside", expr: `file("../nested/deep/secret.txt")`},
|
||||
{name: "whitespace_only_path", expr: `file(" ")`},
|
||||
{name: "dot_path", expr: `file(".")`},
|
||||
{name: "dotdot_path", expr: `file("..")`},
|
||||
{name: "empty_components", expr: `file("////")`},
|
||||
{name: "template_directory", expr: fileExpr(templateDir)},
|
||||
{name: "templates_directory", expr: fileExpr(templatesDir)},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, tc.expr, nil)
|
||||
})
|
||||
require.Error(t, got, "expression %s must fail", tc.expr)
|
||||
require.NotContains(t, got.Error(), "outside-secret")
|
||||
require.NotContains(t, got.Error(), "home-sibling")
|
||||
require.NotContains(t, got.Error(), "prefix-sibling")
|
||||
require.NotContains(t, got.Error(), "nested-secret")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHelperRejectsMissingSandboxContext(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(templateDir, "ok.txt"), []byte("ok"), 0o600))
|
||||
|
||||
_, err := evalFile(t, `file("ok.txt")`, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "no template sandbox context")
|
||||
|
||||
ctxNilOptions := &FileLoadContext{
|
||||
Options: nil,
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
var got error
|
||||
WithFileLoadContext(ctxNilOptions, func() {
|
||||
_, got = evalFile(t, `file("ok.txt")`, nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.Contains(t, got.Error(), "no template sandbox context")
|
||||
}
|
||||
|
||||
func TestFileHelperRejectsWrongArity(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, `file("a","b")`, nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
}
|
||||
|
||||
func TestFileHelperRejectsTemplateAtHomeRootExpandingHome(t *testing.T) {
|
||||
home, templatesDir, _ := setupFileHelperSandbox(t)
|
||||
|
||||
// Place the template directly in $HOME so templateDir == home. Rule 2 must
|
||||
// refuse expanding the allowed tree to the entire home directory.
|
||||
homeTemplate := filepath.Join(home, "template.yaml")
|
||||
require.NoError(t, os.WriteFile(homeTemplate, []byte("id: home-root\n"), 0o600))
|
||||
homeSecret := filepath.Join(home, "ssh-keys.txt")
|
||||
require.NoError(t, os.WriteFile(homeSecret, []byte("id-rsa-material"), 0o600))
|
||||
|
||||
// Sanity-check the sandbox home override is what GetValidAbsPath will see.
|
||||
require.Equal(t, filepath.Clean(home), filepath.Clean(folderutil.HomeDirOrDefault("")),
|
||||
"sandbox home override must be active for this assertion")
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: homeTemplate,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, fileExpr(homeSecret), nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.NotContains(t, got.Error(), "id-rsa-material")
|
||||
}
|
||||
|
||||
func TestFileHelperAllowsNormalizedRelativeWithinTemplateDir(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
require.NoError(t, os.MkdirAll(filepath.Join(templateDir, "payloads"), 0o755))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(templateDir, "payloads", "data.txt"), []byte("in-sandbox"), 0o600))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(templateDir, "root.txt"), []byte("root-payload"), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
expr string
|
||||
want string
|
||||
}{
|
||||
{name: "plain_relative", expr: `file("root.txt")`, want: "root-payload"},
|
||||
{name: "dot_slash", expr: `file("./root.txt")`, want: "root-payload"},
|
||||
{name: "child_relative", expr: `file("payloads/data.txt")`, want: "in-sandbox"},
|
||||
{name: "dotdot_stays_inside", expr: `file("payloads/../root.txt")`, want: "root-payload"},
|
||||
{name: "redundant_dots", expr: `file("./payloads/./data.txt")`, want: "in-sandbox"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var (
|
||||
got string
|
||||
err error
|
||||
)
|
||||
WithFileLoadContext(ctx, func() {
|
||||
got, err = evalFile(t, tc.expr, nil)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHelperNestedContextRestoresOuterSandbox(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
secretDir := t.TempDir()
|
||||
secretPath := filepath.Join(secretDir, "secret.txt")
|
||||
require.NoError(t, os.WriteFile(secretPath, []byte("elevated-only"), 0o600))
|
||||
|
||||
restricted := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
elevated := &FileLoadContext{
|
||||
Options: &types.Options{
|
||||
AllowLocalFileAccess: true,
|
||||
},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
WithFileLoadContext(restricted, func() {
|
||||
var elevatedErr error
|
||||
WithFileLoadContext(elevated, func() {
|
||||
got, err := evalFile(t, fileExpr(secretPath), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "elevated-only", got)
|
||||
})
|
||||
|
||||
_, elevatedErr = evalFile(t, fileExpr(secretPath), nil)
|
||||
require.Error(t, elevatedErr, "outer sandbox must be restored after nested context returns")
|
||||
require.NotContains(t, elevatedErr.Error(), "elevated-only")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileHelperCacheDoesNotCrossContexts(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
secretDir := t.TempDir()
|
||||
secretPath := filepath.Join(secretDir, "secret.txt")
|
||||
require.NoError(t, os.WriteFile(secretPath, []byte("must-not-leak"), 0o600))
|
||||
|
||||
elevated := &FileLoadContext{
|
||||
Options: &types.Options{
|
||||
AllowLocalFileAccess: true,
|
||||
},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
restricted := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
WithFileLoadContext(elevated, func() {
|
||||
got, err := evalFile(t, fileExpr(secretPath), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "must-not-leak", got)
|
||||
})
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(restricted, func() {
|
||||
_, got = evalFile(t, fileExpr(secretPath), nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.NotContains(t, got.Error(), "must-not-leak")
|
||||
}
|
||||
|
||||
func TestFileHelperDeniesSymlinkChainOutsideSandbox(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlink creation is not reliable on all Windows runners")
|
||||
}
|
||||
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
outsideDir := filepath.Join(filepath.Dir(templatesDir), "outside-chain")
|
||||
require.NoError(t, os.MkdirAll(outsideDir, 0o755))
|
||||
outsideFile := filepath.Join(outsideDir, "secret.txt")
|
||||
require.NoError(t, os.WriteFile(outsideFile, []byte("chain-secret"), 0o600))
|
||||
|
||||
midLink := filepath.Join(templatesDir, "mid.link")
|
||||
finalLink := filepath.Join(templatesDir, "final.link")
|
||||
require.NoError(t, os.Symlink(outsideFile, midLink))
|
||||
require.NoError(t, os.Symlink(midLink, finalLink))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, fileExpr(finalLink), nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.NotContains(t, got.Error(), "chain-secret")
|
||||
}
|
||||
|
||||
func TestFileHelperEnforcesExactSizeBoundary(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
|
||||
atLimit := filepath.Join(templateDir, "at-limit.bin")
|
||||
overLimit := filepath.Join(templateDir, "over-limit.bin")
|
||||
|
||||
require.NoError(t, os.WriteFile(atLimit, make([]byte, maxFileHelperSize), 0o600))
|
||||
require.NoError(t, os.WriteFile(overLimit, make([]byte, maxFileHelperSize+1), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
WithFileLoadContext(ctx, func() {
|
||||
got, err := evalFile(t, `file("at-limit.bin")`, nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, int(maxFileHelperSize))
|
||||
|
||||
_, err = evalFile(t, `file("over-limit.bin")`, nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exceeds maximum size")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileHelperPathArgumentDoesNotExecuteAsExpression(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(templateDir, "ok.txt"), []byte("trusted-ok"), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
// Path is opaque data; embedded file(...)-looking text must not open ok.txt.
|
||||
var (
|
||||
got string
|
||||
err error
|
||||
)
|
||||
WithFileLoadContext(ctx, func() {
|
||||
got, err = evalFile(t, `file("prefix file(ok.txt) suffix")`, nil)
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.NotEqual(t, "trusted-ok", got)
|
||||
require.NotContains(t, err.Error(), "trusted-ok")
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build unix
|
||||
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFileHelperDeniesFifo(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
fifoPath := filepath.Join(templateDir, "pipe.fifo")
|
||||
require.NoError(t, syscall.Mkfifo(fifoPath, 0o600))
|
||||
|
||||
// Hold an RDWR handle so the helper's read open does not block waiting for a
|
||||
// writer, and so rejection does not depend on a separate O_WRONLY opener.
|
||||
keepalive, err := os.OpenFile(fifoPath, os.O_RDWR, 0)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = keepalive.Close() })
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, `file("pipe.fifo")`, nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.Contains(t, got.Error(), "not a regular file")
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/projectdiscovery/govaluate"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFileHelperRegistered(t *testing.T) {
|
||||
_, ok := HelperFunctions["file"]
|
||||
require.True(t, ok, "file helper must be registered in HelperFunctions")
|
||||
}
|
||||
|
||||
func TestFileHelperResolvesRelativeAgainstTemplateNotCwd(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(templateDir, "payload.txt"), []byte("template-local"), 0o600))
|
||||
|
||||
originalWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
unrelated := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(unrelated, "payload.txt"), []byte("cwd-local"), 0o600))
|
||||
require.NoError(t, os.Chdir(unrelated))
|
||||
t.Cleanup(func() { _ = os.Chdir(originalWd) })
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var (
|
||||
got string
|
||||
evalErr error
|
||||
)
|
||||
WithFileLoadContext(ctx, func() {
|
||||
got, evalErr = evalFile(t, `file("payload.txt")`, nil)
|
||||
})
|
||||
require.NoError(t, evalErr)
|
||||
require.Equal(t, "template-local", got, "must resolve relative to template dir, not CWD")
|
||||
}
|
||||
|
||||
func TestFileHelperRejectsEmptyPath(t *testing.T) {
|
||||
home, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
_ = home
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, `file("")`, nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.Contains(t, got.Error(), "must not be empty")
|
||||
}
|
||||
|
||||
func TestFileHelperReadsRelativeToTemplateDir(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
payloadPath := filepath.Join(templateDir, "payload.bin")
|
||||
require.NoError(t, os.WriteFile(payloadPath, []byte{0x00, 0x01, 0xff, 'A'}, 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var (
|
||||
got string
|
||||
err error
|
||||
)
|
||||
WithFileLoadContext(ctx, func() {
|
||||
got, err = evalFile(t, `file("payload.bin")`, nil)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string([]byte{0x00, 0x01, 0xff, 'A'}), got)
|
||||
}
|
||||
|
||||
func TestFileHelperReadsFromNucleiTemplatesDirectory(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
helperPath := filepath.Join(templatesDir, "shared-payload.txt")
|
||||
require.NoError(t, os.WriteFile(helperPath, []byte("from-templates-dir"), 0o600))
|
||||
|
||||
// Template lives outside templatesDir; rule 1 still allows templates-dir helpers.
|
||||
outsideHome := t.TempDir()
|
||||
outsideTemplate := filepath.Join(outsideHome, "template.yaml")
|
||||
require.NoError(t, os.WriteFile(outsideTemplate, []byte("id: x\n"), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: outsideTemplate,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
_ = templatePath
|
||||
|
||||
var (
|
||||
got string
|
||||
err error
|
||||
)
|
||||
WithFileLoadContext(ctx, func() {
|
||||
got, err = evalFile(t, fileExpr(helperPath), nil)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "from-templates-dir", got)
|
||||
}
|
||||
|
||||
func TestFileHelperDeniesPathOutsideSandbox(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
secretDir := t.TempDir()
|
||||
secretPath := filepath.Join(secretDir, "secret.txt")
|
||||
require.NoError(t, os.WriteFile(secretPath, []byte("top-secret"), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, fileExpr(secretPath), nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.True(t, strings.Contains(got.Error(), "denied") || strings.Contains(got.Error(), "could not load"), got.Error())
|
||||
}
|
||||
|
||||
func TestFileHelperDeniesTraversalOutsideTemplateDir(t *testing.T) {
|
||||
home, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
outsideHelper := filepath.Join(home, "outside.txt")
|
||||
require.NoError(t, os.WriteFile(outsideHelper, []byte("escaped"), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, `file("../outside.txt")`, nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
}
|
||||
|
||||
func TestFileHelperDeniesSiblingPrefixTemplatesDir(t *testing.T) {
|
||||
home, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
evilDir := filepath.Join(home, "nuclei-templates-evil")
|
||||
require.NoError(t, os.MkdirAll(evilDir, 0o755))
|
||||
evilFile := filepath.Join(evilDir, "payloads.txt")
|
||||
require.NoError(t, os.WriteFile(evilFile, []byte("evil"), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, fileExpr(evilFile), nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
}
|
||||
|
||||
func TestFileHelperDeniesSymlinkToOutside(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlink creation is not reliable on all Windows runners")
|
||||
}
|
||||
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
outsideDir := filepath.Join(filepath.Dir(templatesDir), "outside")
|
||||
require.NoError(t, os.MkdirAll(outsideDir, 0o755))
|
||||
outsideFile := filepath.Join(outsideDir, "secret.txt")
|
||||
require.NoError(t, os.WriteFile(outsideFile, []byte("secret"), 0o600))
|
||||
|
||||
linkPath := filepath.Join(templatesDir, "linked-secret.txt")
|
||||
require.NoError(t, os.Symlink(outsideFile, linkPath))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, fileExpr(linkPath), nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
}
|
||||
|
||||
func TestFileHelperDeniesHardLink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("hard links are not exercised on Windows")
|
||||
}
|
||||
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
outsideDir := t.TempDir()
|
||||
outsideFile := filepath.Join(outsideDir, "secret.txt")
|
||||
require.NoError(t, os.WriteFile(outsideFile, []byte("secret"), 0o600))
|
||||
|
||||
linkPath := filepath.Join(templatesDir, "hardlinked-secret.txt")
|
||||
if err := os.Link(outsideFile, linkPath); err != nil {
|
||||
t.Skipf("hard link not supported: %v", err)
|
||||
}
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, fileExpr(linkPath), nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.Contains(t, got.Error(), "hard link")
|
||||
}
|
||||
|
||||
func TestFileHelperDeniesDirectory(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
dirPath := filepath.Join(filepath.Dir(templatePath), "subdir")
|
||||
require.NoError(t, os.MkdirAll(dirPath, 0o755))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, `file("subdir")`, nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.Contains(t, got.Error(), "not a regular file")
|
||||
}
|
||||
|
||||
func TestFileHelperAllowLocalFileAccessReadsAbsolutePath(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
secretDir := t.TempDir()
|
||||
secretPath := filepath.Join(secretDir, "secret.txt")
|
||||
require.NoError(t, os.WriteFile(secretPath, []byte("allowed-with-lfa"), 0o600))
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{
|
||||
AllowLocalFileAccess: true,
|
||||
},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var (
|
||||
got string
|
||||
err error
|
||||
)
|
||||
WithFileLoadContext(ctx, func() {
|
||||
got, err = evalFile(t, fileExpr(secretPath), nil)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "allowed-with-lfa", got)
|
||||
}
|
||||
|
||||
func TestFileHelperRejectsOversizedContent(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{
|
||||
LoadHelperFileFunction: func(helperFile, _ string, _ catalog.Catalog) (io.ReadCloser, error) {
|
||||
return io.NopCloser(&hugeReader{n: maxFileHelperSize + 1}), nil
|
||||
},
|
||||
},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
var got error
|
||||
WithFileLoadContext(ctx, func() {
|
||||
_, got = evalFile(t, `file("huge.bin")`, nil)
|
||||
})
|
||||
require.Error(t, got)
|
||||
require.Contains(t, got.Error(), "exceeds maximum size")
|
||||
}
|
||||
|
||||
func TestFileHelperCachesPerContext(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
payloadPath := filepath.Join(templateDir, "cached.txt")
|
||||
require.NoError(t, os.WriteFile(payloadPath, []byte("v1"), 0o600))
|
||||
|
||||
var opens int
|
||||
ctx := &FileLoadContext{
|
||||
Options: &types.Options{
|
||||
LoadHelperFileFunction: func(helperFile, tpl string, cat catalog.Catalog) (io.ReadCloser, error) {
|
||||
opens++
|
||||
return (&types.Options{}).LoadHelperFile(helperFile, tpl, cat)
|
||||
},
|
||||
},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
WithFileLoadContext(ctx, func() {
|
||||
got1, err := evalFile(t, `file("cached.txt")`, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "v1", got1)
|
||||
|
||||
got2, err := evalFile(t, `file("cached.txt")`, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "v1", got2)
|
||||
})
|
||||
require.Equal(t, 1, opens, "second file() call should use cache")
|
||||
}
|
||||
|
||||
func TestFileHelperContextDoesNotLeakAcrossGoroutines(t *testing.T) {
|
||||
_, templatesDir, templatePath := setupFileHelperSandbox(t)
|
||||
|
||||
templateDir := filepath.Dir(templatePath)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(templateDir, "ok.txt"), []byte("ok"), 0o600))
|
||||
|
||||
allowedCtx := &FileLoadContext{
|
||||
Options: &types.Options{},
|
||||
TemplatePath: templatePath,
|
||||
Catalog: disk.NewCatalog(templatesDir),
|
||||
}
|
||||
|
||||
started := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(done)
|
||||
WithFileLoadContext(allowedCtx, func() {
|
||||
_, err := evalFile(t, `file("ok.txt")`, nil)
|
||||
require.NoError(t, err)
|
||||
close(started)
|
||||
<-release
|
||||
})
|
||||
}()
|
||||
|
||||
<-started
|
||||
_, errNoCtx := evalFile(t, `file("ok.txt")`, nil)
|
||||
close(release)
|
||||
<-done
|
||||
|
||||
require.Error(t, errNoCtx, "goroutine without context must not observe another goroutine's sandbox")
|
||||
require.Contains(t, errNoCtx.Error(), "no template sandbox context")
|
||||
}
|
||||
|
||||
type hugeReader struct {
|
||||
n int64
|
||||
i int64
|
||||
}
|
||||
|
||||
func (h *hugeReader) Read(p []byte) (int, error) {
|
||||
if h.i >= h.n {
|
||||
return 0, io.EOF
|
||||
}
|
||||
remaining := h.n - h.i
|
||||
if int64(len(p)) > remaining {
|
||||
p = p[:remaining]
|
||||
}
|
||||
for i := range p {
|
||||
p[i] = 'A'
|
||||
}
|
||||
h.i += int64(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func setupFileHelperSandbox(t *testing.T) (home, templatesDir, templatePath string) {
|
||||
t.Helper()
|
||||
|
||||
home = t.TempDir()
|
||||
// os.UserHomeDir (used by folderutil.HomeDirOrDefault) reads HOME on Unix and
|
||||
// USERPROFILE on Windows; set both so sandbox rule-2 home checks are portable.
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
|
||||
templatesDir = filepath.Join(home, "nuclei-templates")
|
||||
templateDir := filepath.Join(home, "custom-templates")
|
||||
for _, dir := range []string{templatesDir, templateDir} {
|
||||
require.NoError(t, os.MkdirAll(dir, 0o755))
|
||||
}
|
||||
|
||||
oldTemplatesDir := config.DefaultConfig.TemplatesDirectory
|
||||
config.DefaultConfig.SetTemplatesDir(templatesDir)
|
||||
t.Cleanup(func() {
|
||||
config.DefaultConfig.SetTemplatesDir(oldTemplatesDir)
|
||||
})
|
||||
|
||||
templatePath = filepath.Join(templateDir, "template.yaml")
|
||||
require.NoError(t, os.WriteFile(templatePath, []byte("id: file-helper\n"), 0o600))
|
||||
return home, templatesDir, templatePath
|
||||
}
|
||||
|
||||
func evalFile(t *testing.T, expression string, values map[string]interface{}) (string, error) {
|
||||
t.Helper()
|
||||
compiled, err := govaluate.NewEvaluableExpressionWithFunctions(expression, HelperFunctions)
|
||||
require.NoError(t, err, "expression must compile")
|
||||
result, err := compiled.Evaluate(values)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return types.ToString(result), nil
|
||||
}
|
||||
|
||||
// fileExpr builds a file() call with a slash-normalized path so Windows
|
||||
// backslashes are not treated as string escapes by the expression parser.
|
||||
func fileExpr(path string) string {
|
||||
return `file("` + filepath.ToSlash(path) + `")`
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// goid returns the current goroutine id.
|
||||
//
|
||||
// DSL helpers have no call-site context argument, so file() scopes its
|
||||
// LoadHelperFile sandbox to the executing goroutine while a template runs.
|
||||
func goid() uint64 {
|
||||
var buf [64]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
s := string(buf[:n])
|
||||
s = strings.TrimPrefix(s, "goroutine ")
|
||||
if i := strings.IndexByte(s, ' '); i > 0 {
|
||||
id, err := strconv.ParseUint(s[:i], 10, 64)
|
||||
if err == nil {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package dsl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestGoidParsesStackHeader fails if the runtime.Stack header format changes,
|
||||
// which would otherwise make file() silently lose its sandbox context.
|
||||
func TestGoidParsesStackHeader(t *testing.T) {
|
||||
id := goid()
|
||||
require.NotZero(t, id)
|
||||
require.Equal(t, id, goid())
|
||||
|
||||
other := make(chan uint64)
|
||||
go func() { other <- goid() }()
|
||||
otherID := <-other
|
||||
require.NotZero(t, otherID)
|
||||
require.NotEqual(t, id, otherID)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package variables
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/invopop/jsonschema"
|
||||
@@ -18,6 +19,9 @@ import (
|
||||
stringsutil "github.com/projectdiscovery/utils/strings"
|
||||
)
|
||||
|
||||
// fileHelperCall matches a file() DSL helper call, not substrings like "profile(".
|
||||
var fileHelperCall = regexp.MustCompile(`\bfile\s*\(`)
|
||||
|
||||
// Variable is a key-value pair of strings that can be used
|
||||
// throughout template.
|
||||
type Variable struct {
|
||||
@@ -239,6 +243,13 @@ func (variables *Variable) checkForLazyEval() bool {
|
||||
return
|
||||
}
|
||||
|
||||
// file() needs the per-template LoadHelperFile sandbox installed during
|
||||
// execution (TemplatePath / Options), so defer evaluation until then.
|
||||
if fileHelperCall.MatchString(types.ToString(value)) {
|
||||
needsLazy = true
|
||||
return
|
||||
}
|
||||
|
||||
if hasUndefinedParams(types.ToString(value), variables) {
|
||||
needsLazy = true
|
||||
return
|
||||
|
||||
@@ -109,6 +109,29 @@ func TestCheckForLazyEval(t *testing.T) {
|
||||
require.False(t, variables.LazyEval, "LazyEval flag should be false")
|
||||
})
|
||||
|
||||
t.Run("file-helper-requires-runtime-sandbox", func(t *testing.T) {
|
||||
variables := &Variable{
|
||||
InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),
|
||||
}
|
||||
variables.Set("payload", `{{file("payload.bin")}}`)
|
||||
|
||||
result := variables.checkForLazyEval()
|
||||
require.True(t, result, "file() needs template sandbox context at execution time")
|
||||
require.True(t, variables.LazyEval, "LazyEval flag should be true for file()")
|
||||
})
|
||||
|
||||
t.Run("literal-text-containing-file-substring-is-not-lazy", func(t *testing.T) {
|
||||
variables := &Variable{
|
||||
InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),
|
||||
}
|
||||
// "profile(" contains the substring "file(" but is not a file() call.
|
||||
variables.Set("label", "user profile(admin)")
|
||||
|
||||
result := variables.checkForLazyEval()
|
||||
require.False(t, result, "literal text containing file( substring must not force lazy eval")
|
||||
require.False(t, variables.LazyEval)
|
||||
})
|
||||
|
||||
t.Run("known-runtime-variables", func(t *testing.T) {
|
||||
// Variables with known runtime variables (Host, BaseURL, etc.) should be lazy
|
||||
variables := &Variable{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -31,6 +32,9 @@ type TemplateExecuter struct {
|
||||
engine TemplateEngine
|
||||
results *atomic.Bool
|
||||
program *goja.Program
|
||||
|
||||
fileLoadCtx *dsl.FileLoadContext
|
||||
fileLoadCtxOnce sync.Once
|
||||
}
|
||||
|
||||
// Both executer & Executor are correct spellings (its open to interpretation)
|
||||
@@ -62,6 +66,14 @@ func NewTemplateExecuter(requests []protocols.Request, options *protocols.Execut
|
||||
|
||||
// Compile compiles the execution generators preparing any requests possible.
|
||||
func (e *TemplateExecuter) Compile() error {
|
||||
var compileErr error
|
||||
dsl.WithFileLoadContext(e.fileLoadContext(), func() {
|
||||
compileErr = e.compile()
|
||||
})
|
||||
return compileErr
|
||||
}
|
||||
|
||||
func (e *TemplateExecuter) compile() error {
|
||||
cliOptions := e.options.Options
|
||||
|
||||
for _, request := range e.requests {
|
||||
@@ -99,6 +111,15 @@ func (e *TemplateExecuter) Requests() int {
|
||||
|
||||
// Execute executes the protocol group and returns true or false if results were found.
|
||||
func (e *TemplateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
var matched bool
|
||||
var execErr error
|
||||
dsl.WithFileLoadContext(e.fileLoadContext(), func() {
|
||||
matched, execErr = e.execute(ctx)
|
||||
})
|
||||
return matched, execErr
|
||||
}
|
||||
|
||||
func (e *TemplateExecuter) execute(ctx *scan.ScanContext) (bool, error) {
|
||||
|
||||
// === when nuclei is built with -tags=stats ===
|
||||
// Note: this is no-op (empty functions) when nuclei is built in normal or without -tags=stats
|
||||
@@ -283,6 +304,15 @@ func getErrorCause(err error) string {
|
||||
|
||||
// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
|
||||
func (e *TemplateExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
var results []*output.ResultEvent
|
||||
var execErr error
|
||||
dsl.WithFileLoadContext(e.fileLoadContext(), func() {
|
||||
results, execErr = e.executeWithResults(ctx)
|
||||
})
|
||||
return results, execErr
|
||||
}
|
||||
|
||||
func (e *TemplateExecuter) executeWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error) {
|
||||
defer e.options.RemoveTemplateCtx(ctx.Input.MetaInput)
|
||||
|
||||
var errx error
|
||||
@@ -306,6 +336,20 @@ func (e *TemplateExecuter) ExecuteWithResults(ctx *scan.ScanContext) ([]*output.
|
||||
return ctx.GenerateResult(), errx
|
||||
}
|
||||
|
||||
func (e *TemplateExecuter) fileLoadContext() *dsl.FileLoadContext {
|
||||
if e == nil || e.options == nil {
|
||||
return nil
|
||||
}
|
||||
e.fileLoadCtxOnce.Do(func() {
|
||||
e.fileLoadCtx = &dsl.FileLoadContext{
|
||||
Options: e.options.Options,
|
||||
TemplatePath: e.options.TemplatePath,
|
||||
Catalog: e.options.Catalog,
|
||||
}
|
||||
})
|
||||
return e.fileLoadCtx
|
||||
}
|
||||
|
||||
// getTemplateType returns the template type of the template
|
||||
func (e *TemplateExecuter) getTemplateType() string {
|
||||
if len(e.requests) == 0 {
|
||||
|
||||
Reference in new issue
Block a user