feat(templates): bind js execution to verified content - #7636
Conversation
Verify signatures before protocol compilation so unsigned init blocks cannot run, and enforce the same check at request execution. Bind cached results to the template, imported file contents, and verifier key. Use the loaded import snapshot for signing and verification, and apply the executable-template re-signing rule to JavaScript. Signed-off-by: Dwi Siswanto <git@dw1.io>
WalkthroughTemplate verification now binds cached metadata to verifier fingerprints and SHA-256 digests of templates and imported files. Compilation and JavaScript execution enforce verification state. Catalog loading revalidates cached metadata when verification values change. ChangesVerified template execution
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant CatalogLoader
participant TemplateParser
participant TemplateSigner
participant JavaScriptExecutor
CatalogLoader->>TemplateParser: load and parse template
TemplateParser->>TemplateSigner: verify template and imported contents
TemplateSigner-->>TemplateParser: return verification metadata
TemplateParser->>JavaScriptExecutor: compile JavaScript initialization
JavaScriptExecutor->>JavaScriptExecutor: execute only when verified
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
pkg/catalog/loader/loader_test.go (1)
526-537: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAdd a verifier-fingerprint-only revalidation test.
This fixture uses a zero-value
VerifierFingerprint. The parsed unsigned template also has a zero-value fingerprint. The incorrectContentDigestalone causes the refresh.Add a case with the correct digest and an intentionally different fingerprint. Assert that the loader refreshes the cached metadata. The PR objective requires cache binding to the verifier key.
🤖 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/catalog/loader/loader_test.go` around lines 526 - 537, Add a revalidation case near the cached metadata fixture using the correct ContentDigest but a deliberately different VerifierFingerprint from the parsed unsigned template. Assert that the loader refreshes the cached metadata, proving verifier-key changes invalidate the cache independently of content. Keep the existing digest-mismatch coverage unchanged.
🤖 Prompt for all review comments with 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.
Inline comments:
In `@pkg/protocols/javascript/js.go`:
- Around line 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.
---
Nitpick comments:
In `@pkg/catalog/loader/loader_test.go`:
- Around line 526-537: Add a revalidation case near the cached metadata fixture
using the correct ContentDigest but a deliberately different VerifierFingerprint
from the parsed unsigned template. Assert that the loader refreshes the cached
metadata, proving verifier-key changes invalidate the cache independently of
content. Keep the existing digest-mismatch coverage unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 87fb16bd-2c07-4425-87bf-76d30484e0e1
📒 Files selected for processing (14)
pkg/catalog/index/index.gopkg/catalog/index/metadata.gopkg/catalog/loader/loader.gopkg/catalog/loader/loader_test.gopkg/protocols/javascript/js.gopkg/protocols/javascript/js_test.gopkg/protocols/protocols.gopkg/templates/compile.gopkg/templates/compile_test.gopkg/templates/fuzz_harness.gopkg/templates/signer/tmpl_signer.gopkg/templates/signer/tmpl_signer_test.gopkg/templates/template_sign_test.gopkg/templates/templates.go
| if request.options == nil || !request.options.Verified { | ||
| return errkit.New("refusing to execute unverified javascript template; sign it (-sign) or run a verified template") | ||
| } | ||
|
|
There was a problem hiding this comment.
🩺 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 -1200Repository: 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 -1800Repository: 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 -2200Repository: 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 -2400Repository: 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)
PYRepository: 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.
|
@coderabbitai create an issue to track this. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="projectdiscovery/nuclei"
pr_number="7636"
echo "== Issue comments =="
gh api "repos/$repo/issues/$pr_number/comments?per_page=100" \
--jq '.[] | {id, html_url, user: .user.login, body, created_at}'
echo "== Pull-request review comments =="
gh api "repos/$repo/pulls/$pr_number/comments?per_page=100" \
--jq '.[] | {id, html_url, user: .user.login, path, line, original_line, body, created_at}'
echo "== Pull-request reviews =="
gh api "repos/$repo/pulls/$pr_number/reviews?per_page=100" \
--jq '.[] | {id, html_url, user: .user.login, state, body, submitted_at}'Length of output: 19968 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="projectdiscovery/nuclei"
body=$(cat <<'EOF'
## Summary
`NucleiEngine.ParseTemplate` bypasses the catalog loader. An unsigned JavaScript template can therefore reach JavaScript request execution through the SDK parse path.
## Required changes
Add SDK-level validation or skipping for unsigned JavaScript templates before target execution in the `NucleiEngine.ParseTemplate` flow.
Keep the existing `request.options.Verified` check in `pkg/protocols/javascript/js.go` as a fail-closed execution safeguard.
## Rationale
The catalog loader rejects unsigned JavaScript templates. The SDK parse path must apply equivalent protection so unsigned JavaScript does not proceed to per-target execution.
## Affected areas
- `lib/sdk.go`: `NucleiEngine.ParseTemplate`
- `pkg/protocols/javascript/js.go`: `Request.ExecuteWithResults` verification gate
## Acceptance criteria
- The SDK parse path validates or skips unsigned JavaScript templates before target execution.
- The JavaScript execution gate continues to reject nil or unverified executor options.
- Tests cover the SDK parse path for an unsigned JavaScript template.
## Backlinks
- Pull request: https://github.com/projectdiscovery/nuclei/pull/7636
- CodeRabbit review comment: https://github.com/projectdiscovery/nuclei/pull/7636#discussion_r3709243018
- Requested by: `@dwisiswant0`
EOF
)
gh issue create \
--repo "$repo" \
--title "Harden the SDK parse path for unsigned JavaScript templates" \
--body "$body" \
--assignee "dwisiswant0"Length of output: 1736
You are interacting with an AI system. |
Proposed changes
Verify signatures before protocol compilation so
unsigned init blocks cannot run, and enforce the
same check at request execution.
Bind cached results to the template, imported file
contents, and verifier key. Use the loaded import
snapshot for signing and verification, and apply
the executable-template re-signing rule to
JavaScript.
Proof
Checklist
Summary by CodeRabbit