Skip to content
Merged
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
4 changes: 3 additions & 1 deletion pkg/catalog/index/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const (

// IndexVersion is the schema version for cache invalidation on breaking
// changes.
IndexVersion = 1
IndexVersion = 2

// DefaultMaxSize is the default maximum number of templates to cache.
DefaultMaxSize = 50000
Expand Down Expand Up @@ -72,6 +72,8 @@ func NewIndex(cacheDir string) (*Index, error) {
weight += len(value.Severity)
weight += len(value.ProtocolType)
weight += len(value.TemplateVerifier)
weight += len(value.VerifierFingerprint)
weight += len(value.ContentDigest)

for _, author := range value.Authors {
weight += len(author)
Expand Down
13 changes: 11 additions & 2 deletions pkg/catalog/index/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ type Metadata struct {
// TemplateVerifier is the verifier used for the template.
TemplateVerifier string `gob:"verifier,omitempty"`

// VerifierFingerprint identifies the public key that verified the template.
VerifierFingerprint [32]byte `gob:"verifier_fingerprint,omitempty"`

// ContentDigest binds the cached verification result to the content that
// was verified.
ContentDigest [32]byte `gob:"content_digest,omitempty"`

// Validation records how the built-in parser validated this metadata's
// source before it was cached.
Validation ValidationMode `gob:"validation,omitempty"`
Expand Down Expand Up @@ -108,8 +115,10 @@ func NewMetadataFromTemplate(path string, tpl *templates.Template) *Metadata {

ProtocolType: tpl.Type().String(),

Verified: tpl.Verified,
TemplateVerifier: tpl.TemplateVerifier,
Verified: tpl.Verified,
TemplateVerifier: tpl.TemplateVerifier,
VerifierFingerprint: tpl.VerifierFingerprint(),
ContentDigest: tpl.ContentDigest(),
}
}

Expand Down
13 changes: 10 additions & 3 deletions pkg/catalog/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,10 @@ func getTemplateVerification(metadataIndex *index.Index, templatePath string) *p
}

return &protocols.TemplateVerification{
Verified: metadata.Verified,
Verifier: metadata.TemplateVerifier,
Verified: metadata.Verified,
Verifier: metadata.TemplateVerifier,
VerifierFingerprint: metadata.VerifierFingerprint,
ContentDigest: metadata.ContentDigest,
}
}

Expand Down Expand Up @@ -891,7 +893,12 @@ func (store *Store) LoadTemplatesWithTags(templatesList, tags []string) ([]*temp
if loaded {
parsed, err := templates.Parse(templatePath, store.preprocessor, store.config.ExecutorOptions)

if parsed != nil && !metadataReusable {
verificationChanged := parsed != nil && (metadata == nil ||
metadata.Verified != parsed.Verified ||
metadata.TemplateVerifier != parsed.TemplateVerifier ||
metadata.VerifierFingerprint != parsed.VerifierFingerprint() ||
metadata.ContentDigest != parsed.ContentDigest())
if parsed != nil && (!metadataReusable || verificationChanged) {
if store.metadataIndex != nil {
metadata = store.cacheValidatedMetadata(templatePath, parsed)
} else {
Expand Down
72 changes: 72 additions & 0 deletions pkg/catalog/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"path/filepath"
"reflect"
"testing"
"time"

"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gologger/formatter"
Expand Down Expand Up @@ -500,6 +501,77 @@ javascript:
require.Equal(t, initialUnverified, stats.GetValue(templates.SkippedUnverifiedTemplateStats))
}

func TestLoadTemplatesReverifiesCachedJavascriptTemplate(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "cached-javascript.yaml")
verifiedModTime := time.Now().Add(-time.Hour)
require.NoError(t, os.WriteFile(templatePath, []byte(`id: cached-javascript

info:
name: Cached Javascript
author: pdteam
severity: info

javascript:
- init: |
set("init-status", "executed")
code: |
Export("cached-javascript")
`), 0o600))
require.NoError(t, os.Chtimes(templatePath, verifiedModTime, verifiedModTime))
fileInfo, err := os.Stat(templatePath)
require.NoError(t, err)

metadataIndex, err := metadataindex.NewIndex(t.TempDir())
require.NoError(t, err)
metadataIndex.Set(templatePath, &metadataindex.Metadata{
ID: "cached-javascript",
FilePath: templatePath,
ModTime: fileInfo.ModTime(),
Name: "Cached Javascript",
Authors: []string{"pdteam"},
Severity: "info",
ProtocolType: "javascript",
Verified: true,
TemplateVerifier: "projectdiscovery/nuclei-templates",
ContentDigest: [32]byte{1},
Validation: metadataindex.ValidationStrict,
})

options := testutils.DefaultOptions.Copy()
options.Logger = &gologger.Logger{}
options.ExecutionId = "loader-cached-javascript"
options.DisableUnsignedTemplates = false
options.TemplateLoadingConcurrency = 1
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})

catalog := disk.NewCatalog("")
executerOpts := testutils.NewMockExecuterOptions(options, nil)
executerOpts.Catalog = catalog
executerOpts.Parser = templates.NewParser()
executerOpts.Logger = options.Logger

workflowLoader, err := workflow.NewLoader(executerOpts)
require.NoError(t, err)
executerOpts.WorkflowLoader = workflowLoader

loaderConfig := NewConfig(options, catalog, executerOpts)
loaderConfig.MetadataIndex = metadataIndex
store, err := New(loaderConfig)
require.NoError(t, err)

loaded, err := store.LoadTemplates([]string{templatePath})
require.NoError(t, err)
require.Empty(t, loaded, "mtime-only metadata must not authorize javascript execution")

refreshedMetadata, found := metadataIndex.Get(templatePath)
require.True(t, found)
require.False(t, refreshedMetadata.Verified)
require.NotEqual(t, [32]byte{1}, refreshedMetadata.ContentDigest)
}

func TestLoadTemplatesTreatsMixedTemplateWithJavascriptAsJavascriptSensitive(t *testing.T) {
templatePath := filepath.Join(t.TempDir(), "mixed-javascript.yaml")
err := os.WriteFile(templatePath, []byte(`id: mixed-javascript-template
Expand Down
19 changes: 15 additions & 4 deletions pkg/protocols/javascript/js.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,18 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
}
}

var initCompiled *goja.Program

if request.Init != "" {
// Validate init syntax for every template, but do not execute unsigned
// template code during compilation.
initCompiled, err = compiler.SourceAutoMode(request.Init, false)
if err != nil {
return errkit.Newf("could not compile init code: %s", err)
}
}

if initCompiled != nil && request.options.Verified {
// execute init code if any
if request.options.Options.Debug || request.options.Options.DebugRequests {
gologger.Debug().Msgf("[%s] Executing Template Init\n", request.TemplateID)
Expand Down Expand Up @@ -220,10 +231,6 @@ func (request *Request) Compile(options *protocols.ExecutorOptions) error {
// proceed with whatever args we have
args.Args, _, _ = request.evaluateArgs(allVars, options, true)

initCompiled, err := compiler.SourceAutoMode(request.Init, false)
if err != nil {
return errkit.Newf("could not compile init code: %s", err)
}
result, err := request.options.JsCompiler.ExecuteWithOptions(context.Background(), initCompiled, args, opts)
if err != nil {
return errkit.Newf("could not execute pre-condition: %s", err)
Expand Down Expand Up @@ -285,6 +292,10 @@ func (request *Request) GetID() string {

// ExecuteWithResults executes the protocol requests and returns results instead of writing them.
func (request *Request) ExecuteWithResults(target *contextargs.Context, dynamicValues, previous output.InternalEvent, callback protocols.OutputEventCallback) error {
if request.options == nil || !request.options.Verified {
return errkit.New("refusing to execute unverified javascript template; sign it (-sign) or run a verified template")
}

Comment on lines +295 to +298

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Trace unsigned-JavaScript gates and callers that can reach javascript Request.ExecuteWithResults.
set -uo pipefail

# 1. Where is the unsigned-javascript loader gate applied?
rg -n -C5 'IsUnsignedJavascriptTemplate|SkippedUnverifiedJavascriptTemplateStats' --type=go

# 2. Who propagates ExecutorOptions.Verified into protocol requests?
rg -n -C4 '\bVerified\b' --type=go -g '!**/*_test.go' pkg/protocols pkg/templates pkg/tmplexec 2>/dev/null

# 3. Callers of ExecuteWithResults on protocol requests (executer/flow/multiproto paths).
rg -n -C4 '\.ExecuteWithResults\(' --type=go -g '!**/*_test.go'

# 4. Does the SDK expose a path that compiles/executes javascript requests without templates.Parse?
rg -n -C4 'javascript\.Request|JavascriptProtocol' --type=go -g '!**/*_test.go'

Repository: projectdiscovery/nuclei

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(pkg/protocols/javascript/js\.go|.*(template|exec|flow|cluster|sdk|protocol).*\.go$)' | head -200

printf '%s\n' '--- JavaScript protocol symbols ---'
rg -n -C6 'ExecuteWithResults|IsUnsignedJavascriptTemplate|type Request|Verified|javascript\.Request|JavascriptProtocol' pkg --glob '*.go' 2>/dev/null | head -1200

printf '%s\n' '--- all ExecuteWithResults callers ---'
rg -n -C5 '\.ExecuteWithResults\(' --glob '*.go' --glob '!**/*_test.go' . 2>/dev/null | head -1200

Repository: projectdiscovery/nuclei

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- unsigned-template filtering ---'
rg -n -C8 'IsUnsignedJavascriptTemplate|SkippedUnverifiedJavascript|disable-unsigned|UnsignedJavascript|SkippedUnverifiedTemplate' \
  internal pkg lib --glob '*.go' 2>/dev/null | head -1600

printf '%s\n' '--- JavaScript request execution call sites ---'
rg -n -C8 'RequestsJavascript|JavascriptProtocol|javascript\.Request|ExecuteWithResults\(' \
  internal pkg lib --glob '*.go' --glob '!**/*_test.go' 2>/dev/null |
  rg -n -C5 'javascript|Javascript|ExecuteWithResults|RequestsJavascript|ClusterExecuter|Flow|Workflow|SDK|request' |
  head -2200

printf '%s\n' '--- relevant executor option propagation ---'
rg -n -C10 'options\.Verified|Verified *=|Verified:' \
  pkg/templates pkg/core internal/runner internal/server lib --glob '*.go' --glob '!**/*_test.go' 2>/dev/null | head -1800

Repository: projectdiscovery/nuclei

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- workflow loader and template execution ---'
sed -n '1,145p' pkg/templates/workflows.go
sed -n '300,375p' pkg/templates/compile.go
sed -n '220,370p' pkg/templates/cluster.go

printf '%s\n' '--- request container and executor construction ---'
rg -n -C8 'type .*Executer|type .*Request|NewTemplateExecuter|ExecuteWithResults\(inputItem|requests \[\]|requests\.' \
  pkg/tmplexec pkg/templates --glob '*.go' --glob '!**/*_test.go' 2>/dev/null | head -1800

printf '%s\n' '--- SDK/template loading and execution entry points ---'
rg -n -C8 'ParseTemplate|LoadTemplates|ExecuteWithResults|Execute\(|New.*Execut|Template.*Options|DisableUnsignedTemplates' \
  lib internal/server internal/runner --glob '*.go' --glob '!**/*_test.go' 2>/dev/null | head -2200

Repository: projectdiscovery/nuclei

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- public SDK execution APIs ---'
rg -n -C12 'func (.*)(Parse|Load|Execute|Scan|Template)|ParseTemplate|LoadTemplates|TemplateLoader|ExecutorOptions|DisableUnsignedTemplates' \
  lib/sdk.go lib/sdk_private.go internal/server/nuclei_sdk.go --glob '*.go' 2>/dev/null | head -2400

printf '%s\n' '--- Parse call sites outside the catalog loader ---'
rg -n -C6 '\btemplates\.Parse\(|ParseTemplateFromReader|NewTemplateExecuter|NewFlowExecutor|NewMultiProtocol|NewGenericEngine' \
  lib internal pkg --glob '*.go' --glob '!**/*_test.go' 2>/dev/null | head -2400

printf '%s\n' '--- workflow top-level loading and execution ---'
rg -n -C10 'WorkflowLoader|CompiledWorkflow|Executers|ProtocolExecuterPair|parseWorkflowTemplate|Load.*Workflow' \
  pkg/loader pkg/core pkg/workflows internal lib --glob '*.go' --glob '!**/*_test.go' 2>/dev/null | head -2400

Repository: projectdiscovery/nuclei

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- direct SDK parse/compile path ---'
sed -n '431,490p' pkg/templates/compile.go
sed -n '200,214p' lib/sdk.go
sed -n '145,175p' internal/server/nuclei_sdk.go

printf '%s\n' '--- SDK consumers of ParseTemplate and exported executers ---'
rg -n -C10 'ParseTemplate\(|\.Executer|GetExecuterOptions\(' \
  lib internal --glob '*.go' 2>/dev/null | head -1800

printf '%s\n' '--- source-level path verifier ---'
python3 - <<'PY'
from pathlib import Path

loader = Path("pkg/catalog/loader/loader.go").read_text()
workflow = Path("pkg/templates/workflows.go").read_text()
sdk = Path("lib/sdk.go").read_text()
compile_src = Path("pkg/templates/compile.go").read_text()
js = Path("pkg/protocols/javascript/js.go").read_text()

checks = {
    "loader rejects unsigned JavaScript": "if parsed.IsUnsignedJavascriptTemplate()" in loader,
    "workflow rejects unsigned JavaScript": "if template.IsUnsignedJavascriptTemplate()" in workflow,
    "SDK exposes direct ParseTemplate": "func (e *NucleiEngine) ParseTemplate(data []byte)" in sdk,
    "SDK ParseTemplate bypasses catalog loader": "templates.ParseTemplateFromReader(bytes.NewReader(data), nil, e.executerOpts)" in sdk,
    "direct compile includes JavaScript requests": "template.HasJavascriptRequest()" in compile_src,
    "execution gate rejects nil/unverified options": "if request.options == nil || !request.options.Verified" in js,
}
for name, ok in checks.items():
    print(f"{name}: {'YES' if ok else 'NO'}")
if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: projectdiscovery/nuclei

Length of output: 8958


Handle unsigned JavaScript in the SDK parse path. NucleiEngine.ParseTemplate bypasses the catalog loader and returns a compiled template that can reach this gate for every target. Add one SDK-level validation or skip before target execution. Keep this fail-closed gate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/protocols/javascript/js.go` around lines 295 - 298, Update the SDK parse
path in NucleiEngine.ParseTemplate to validate or skip unsigned JavaScript
templates before they reach target execution, while preserving the existing
request.options verification gate as a fail-closed safeguard.

// Get default port(s) if specified in template
ports := request.getPorts()
if len(ports) == 0 {
Expand Down
23 changes: 22 additions & 1 deletion pkg/protocols/javascript/js_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
Expand All @@ -15,7 +16,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
javascript "github.com/projectdiscovery/nuclei/v3/pkg/protocols/javascript"
"github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/internal/tests/testutils"
"github.com/projectdiscovery/ratelimit"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -89,6 +89,7 @@ func TestExecuteWithResultsReturnsArgEvaluationErrorWithoutPanic(t *testing.T) {

executorOptions := testutils.NewMockExecuterOptions(options, tmplInfo)
executorOptions.JsCompiler = templates.GetJsCompiler()
executorOptions.Verified = true

request := &javascript.Request{
Args: map[string]interface{}{
Expand All @@ -108,3 +109,23 @@ func TestExecuteWithResultsReturnsArgEvaluationErrorWithoutPanic(t *testing.T) {
})
require.ErrorContains(t, err, `failed to evaluate expression "base64()"`)
}

func TestExecuteWithResultsRejectsUnverifiedTemplate(t *testing.T) {
options := testutils.DefaultOptions.Copy()
testutils.Init(options)
t.Cleanup(func() {
testutils.Cleanup(options)
})

executorOptions := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{ID: "unverified-javascript"})
executorOptions.JsCompiler = templates.GetJsCompiler()

request := &javascript.Request{Code: `module.exports = { success: true, response: "unexpected" }`}
require.NoError(t, request.Compile(executorOptions))

target := contextargs.NewWithInput(context.Background(), "https://example.com:443")
err := request.ExecuteWithResults(target, nil, nil, func(*output.InternalWrappedEvent) {
t.Fatal("unexpected callback for unverified javascript template")
})
require.ErrorContains(t, err, "refusing to execute unverified javascript template")
}
14 changes: 9 additions & 5 deletions pkg/protocols/protocols.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@ type Executer interface {

// TemplateVerification holds cached verification information for a template.
type TemplateVerification struct {
Verified bool
Verifier string
Verified bool
Verifier string
VerifierFingerprint [32]byte

// ContentDigest binds the cached result to the verified template and
// imported-file contents.
ContentDigest [32]byte
}

// ExecutorOptions contains the configuration options for executer clients
Expand All @@ -73,9 +78,8 @@ type ExecutorOptions struct {
TemplateInfo model.Info
// TemplateVerifier is the verifier for the template
TemplateVerifier string
// Verified reports whether the template's signature was successfully
// verified by a trusted verifier. It is checked by the code protocol at
// execution time.
// Verified reports whether a trusted verifier verified the template's
// signature. Code and JavaScript protocols check it at execution time.
Verified bool
// TemplateVerificationCallback returns cached verification info for a template path.
// If it returns nil, verification should be computed normally.
Expand Down
Loading
Loading