Skip to content

refactor: use path-aware filesystem containment checks - #7420

Merged
ehsandeep merged 5 commits into
devfrom
dwisiswant0/refactor/use-path-aware-filesystem-containment-checks
Jun 4, 2026
Merged

refactor: use path-aware filesystem containment checks#7420
ehsandeep merged 5 commits into
devfrom
dwisiswant0/refactor/use-path-aware-filesystem-containment-checks

Conversation

@dwisiswant0

@dwisiswant0 dwisiswant0 commented May 22, 2026

Copy link
Copy Markdown
Member

Proposed changes

Several filesystem trust boundaries relied on
lexical prefix checks to decide whether a path
fell under an allowed directory. That let sibling
paths be treated as if they were children of
trusted directories.

Replace those checks with canonical path
containment checks for helper payload loading,
template archive extraction, custom template
metadata, template path classification, and
headless screenshot output validation.

Proof

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • Bug Fixes
    • More accurate custom-vs-official template detection to avoid sibling-prefix misclassification and ensure correct install/cleanup/checksum behavior.
  • Security
    • Hardened directory-boundary, symlink and path validation for installs, downloads, screenshot/file outputs, and exports to prevent path-traversal and LFI.
  • Tests
    • Broad added unit and fuzz tests covering template detection, safe path joins, installer behavior, screenshot gating, checksum/cleanup, and filename sanitization.

Review Change Stack

Signed-off-by: Dwi Siswanto <git@dw1.io>
@auto-assign
auto-assign Bot requested a review from Mzack9999 May 22, 2026 13:47
@dwisiswant0

Copy link
Copy Markdown
Member Author

@coderabbitai create an issue for this. prefixed with "[BUG]". it's about correctness.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo projectdiscovery/nuclei \
  --title "[BUG] Incorrect lexical prefix checks allow path containment bypass in filesystem trust boundaries" \
  --body "## Description

Several filesystem trust boundary validations in nuclei relied on **lexical prefix checks** to determine whether a path falls under an allowed directory. This approach is incorrect because sibling paths with a common prefix can be incorrectly treated as children of trusted directories, leading to path containment bypass.

## Affected Areas

- Helper payload loading
- Template archive extraction
- Custom template metadata
- Template path classification
- Headless screenshot output validation

## Expected Behavior

Path containment checks should use **canonical (resolved) paths** to correctly determine whether a given path is a child of a trusted directory, accounting for symlinks, \`.\`, \`..\`, and similar edge cases.

## Actual Behavior

Lexical string prefix checks (e.g., \`strings.HasPrefix\`) are used instead of canonical path resolution. This means a path like \`/trusted-dir-evil\` could be mistakenly accepted as being under \`/trusted-dir\` depending on the implementation.

## References

- Related PR: https://github.com/projectdiscovery/nuclei/pull/7420
- Reported by: `@dwisiswant0`" \
  --assignee dwisiswant0

Length of output: 55


@dwisiswant0: Done! I've created the issue here: #7421

[BUG] Incorrect lexical prefix checks allow path containment bypass in filesystem trust boundaries — it covers the correctness problem across all five affected areas (helper payload loading, template archive extraction, custom template metadata, template path classification, and headless screenshot output validation), with a backlink to this PR.

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da499739-ebe1-490d-9071-0ae75f347d6c

📥 Commits

Reviewing files that changed from the base of the PR and between ccc35a5 and c11171c.

📒 Files selected for processing (1)
  • pkg/utils/filepath/filepath_test.go

Walkthrough

Replace lexical prefix-based path checks with canonical filesystem containment checks (IsPathWithinDirectory / IsPathWithinAnyDirectory) across utilities, catalog, installer, external custom template downloaders, headless screenshot gating, helper validation, and reporting; add tests and fuzz coverage.

Changes

Path-Containment Security Fixes

Layer / File(s) Summary
Path-within-directory utility functions
pkg/utils/filepath/filepath.go, pkg/utils/filepath/filepath_test.go
Add IsPathWithinAnyDirectory, make IsPathWithinDirectory fail closed for empty inputs, improve symlink canonicalization fallback, and add unit/fuzz tests.
Catalog config template classification and filtering
pkg/catalog/config/nucleiconfig.go, pkg/catalog/config/nucleiconfig_test.go, pkg/catalog/config/template.go, pkg/catalog/config/template_test.go
IsCustomTemplate and index filtering now use containment checks; IsTemplateWithRoot tightens .. handling; tests validate sibling-prefix boundary cases.
Embedded FS glob resolution (catalog disk)
pkg/catalog/disk/find.go, pkg/catalog/disk/find_test.go
Resolve embedded FS glob relative paths with containment + filepath.Rel and normalize to slash-separated paths; tests ensure sibling-prefix globs don't alias into templates root.
Catalog disk path imports / removed helper
pkg/catalog/disk/path.go
Adjust imports and remove legacy BackwardsCompatiblePaths export.
Template installer path validation and cleanup
pkg/installer/template.go, pkg/installer/template_test.go, pkg/installer/zipslip_unix_test.go
Validate archive entry write paths remain within templates dir (including parent checks); skip configured custom template dirs using IsPathWithinAnyDirectory; add cleanup/checksum and ZipSlip tests for traversal and sibling-prefix cases.
S3 safe join and tests (external/customtemplates)
pkg/external/customtemplates/s3.go, pkg/external/customtemplates/s3_test.go
Add safeJoinWithinDirectory used by S3 downloads to reject traversal keys; extensive unit and fuzz tests for traversal, sibling-prefix, symlink, leading-slash, and empty-base cases.
GitLab/Azure safe output paths (external/customtemplates)
pkg/external/customtemplates/gitlab.go, pkg/external/customtemplates/azure_blob.go, tests
Compute safe project and per-file output paths, skip unsafe server-provided paths, preserve nested structure when writing, and skip unsafe Azure blob names; add unit tests for GitLab helpers.
Helper file path validation in Options
pkg/types/types.go, pkg/types/types_test.go
GetValidAbsPath and isHomeDir use containment checks instead of prefix logic; tests cover allowed, sibling-prefix rejection, and symlink escape cases.
Template path URL relative-path formatting
pkg/utils/template_path.go, pkg/utils/template_path_test.go
Relativize template paths using containment + filepath.Rel only for in-directory files; test sibling-prefix non-relativization.
Reporting markdown filename sanitizer
pkg/reporting/exporters/markdown/markdown.go, pkg/reporting/exporters/markdown/markdown_test.go
Harden sanitizeFilename to strip Windows separators and ..; add unit and fuzz tests ensuring sanitized subdirectory stays within report directory.

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly Related Issues

Suggested reviewers

  • dogancanbakir
  • Mzack9999

Poem

🐰 I hopped through paths both near and wide,

Guarded roots where tricksy prefixes hide,
Symlinks and siblings met their match,
Boundaries checked, no secret latch,
A safe small hop — the forest peaceful, inside.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: replacing lexical prefix checks with canonical path-aware containment checks across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dwisiswant0/refactor/use-path-aware-filesystem-containment-checks

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/protocols/headless/engine/page_actions.go (1)

526-559: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Validate the final screenshot path, not the pre-extension input.

Line 531 checks to, but Lines 551-553 can still move the write outside cwd by appending .png afterward. For example, to="." passes the containment check and then becomes <cwd>.png, which is a sibling path outside the allowed directory.

🔒 Proposed fix
 	to, err = fileutil.CleanPath(to)
 	if err != nil {
 		return errkit.Newf("could not clean output screenshot path %s", to)
 	}
 
-	if err := p.isScreenshotPathAllowed(to); err != nil {
-		return err
-	}
+	filePath := to
+	if !strings.HasSuffix(filePath, ".png") {
+		filePath += ".png"
+	}
+	if err := p.isScreenshotPathAllowed(filePath); err != nil {
+		return err
+	}
 
 	mkdir, err := p.getActionArg(act, "mkdir")
 	if err != nil {
 		return err
 	}
@@
-	if mkdir == "true" && stringsutil.ContainsAny(to, folderutil.UnixPathSeparator, folderutil.WindowsPathSeparator) {
+	if mkdir == "true" && stringsutil.ContainsAny(filePath, folderutil.UnixPathSeparator, folderutil.WindowsPathSeparator) {
 		// creates new directory if needed based on path `to`
 		// TODO: replace all permission bits with fileutil constants (https://github.com/projectdiscovery/utils/issues/113)
-		if err := os.MkdirAll(filepath.Dir(to), 0700); err != nil {
+		if err := os.MkdirAll(filepath.Dir(filePath), 0700); err != nil {
 			return errkit.Wrap(err, "failed to create directory while writing screenshot")
 		}
 	}
-
-	// actual file path to write
-	filePath := to
-	if !strings.HasSuffix(filePath, ".png") {
-		filePath += ".png"
-	}
🤖 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/headless/engine/page_actions.go` around lines 526 - 559, The
code currently validates the input `to` before appending the ".png" extension,
which lets inputs like "." slip past containment checks; update the logic to
build the final path first (variable `filePath`), appending ".png" if needed,
then call `p.isScreenshotPathAllowed(filePath)` to validate the actual write
target; also use `filepath.Dir(filePath)` when creating directories (the `mkdir`
branch) so directory creation and the containment check operate on the same
final path before calling `os.WriteFile`.
pkg/types/types.go (1)

886-900: ⚠️ Potential issue | 🟠 Major

Fix helper-path resolution to avoid process CWD dependence
In pkg/types/types.go’s GetValidAbsPath, the fallback branch does fileutil.CleanPath(helperFilePath); fileutil.CleanPath resolves non-absolute paths using os.Getwd() (process CWD). Relative helper references passed from templates are not normalized against filepath.Dir(templatePath) before LoadHelperFile (payloads pass the raw string; ImportFileRefs only gates via fileutil.FileExists(request.Source/request.Code) on the raw value). This means a helper like payloads.txt intended to be relative to the template directory will be interpreted relative to CWD whenever ResolveNClean(...GetTemplateDir()) doesn’t succeed.

  • Resolve relative helper paths against filepath.Dir(templatePath) (or otherwise pass an explicit base) before sandbox checks, instead of relying on fileutil.CleanPath’s CWD behavior.
🤖 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/types/types.go` around lines 886 - 900, GetValidAbsPath currently calls
fileutil.CleanPath(helperFilePath) which resolves relative paths against process
CWD; instead, detect non-absolute helperFilePath in GetValidAbsPath and join it
with filepath.Dir(templatePath) (or call ResolveNClean with the template dir as
base) before cleaning and sandbox/existence checks so helper references like
"payloads.txt" are resolved relative to the template directory; update any
subsequent uses (e.g., LoadHelperFile, ImportFileRefs) to use this resolved path
and preserve existing error wrapping via errkit.Wrapf.
🤖 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/installer/template.go`:
- Around line 281-283: The path containment check currently calls
filepathutil.IsPathWithinDirectory on newPath and filepath.Dir(newPath), but
both can be non-existent and miss a symlinked ancestor; change the check to walk
upward from newPath to the nearest existing ancestor (using os.Stat or similar)
and call filepathutil.IsPathWithinDirectory against that existing parent and
templateDir before proceeding; update the code paths that call
CreateFolder/WriteFile to rely on this new validation and add a regression test
that creates a symlinked child directory (e.g., templateDir/link -> /outside)
and asserts that operations like CreateFolder/WriteFile are rejected.

---

Outside diff comments:
In `@pkg/protocols/headless/engine/page_actions.go`:
- Around line 526-559: The code currently validates the input `to` before
appending the ".png" extension, which lets inputs like "." slip past containment
checks; update the logic to build the final path first (variable `filePath`),
appending ".png" if needed, then call `p.isScreenshotPathAllowed(filePath)` to
validate the actual write target; also use `filepath.Dir(filePath)` when
creating directories (the `mkdir` branch) so directory creation and the
containment check operate on the same final path before calling `os.WriteFile`.

In `@pkg/types/types.go`:
- Around line 886-900: GetValidAbsPath currently calls
fileutil.CleanPath(helperFilePath) which resolves relative paths against process
CWD; instead, detect non-absolute helperFilePath in GetValidAbsPath and join it
with filepath.Dir(templatePath) (or call ResolveNClean with the template dir as
base) before cleaning and sandbox/existence checks so helper references like
"payloads.txt" are resolved relative to the template directory; update any
subsequent uses (e.g., LoadHelperFile, ImportFileRefs) to use this resolved path
and preserve existing error wrapping via errkit.Wrapf.
🪄 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: e38b910f-424b-4dd6-8b22-008476bc4461

📥 Commits

Reviewing files that changed from the base of the PR and between 7ee7cca and fd9bb10.

📒 Files selected for processing (15)
  • pkg/catalog/config/nucleiconfig.go
  • pkg/catalog/config/nucleiconfig_test.go
  • pkg/catalog/config/template.go
  • pkg/catalog/config/template_test.go
  • pkg/installer/template.go
  • pkg/installer/template_test.go
  • pkg/installer/zipslip_unix_test.go
  • pkg/protocols/headless/engine/page_actions.go
  • pkg/protocols/headless/engine/page_actions_test.go
  • pkg/types/types.go
  • pkg/types/types_test.go
  • pkg/utils/filepath/filepath.go
  • pkg/utils/filepath/filepath_test.go
  • pkg/utils/template_path.go
  • pkg/utils/template_path_test.go

Comment thread pkg/installer/template.go
Comment on lines +281 to 283
if !filepathutil.IsPathWithinDirectory(newPath, templateDir) || !filepathutil.IsPathWithinDirectory(filepath.Dir(newPath), templateDir) {
// we don't allow LFI
return ""

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Validate against the nearest existing ancestor, not just filepath.Dir(newPath).

For entries like root/link/new/file.yaml, both newPath and filepath.Dir(newPath) can be non-existent. If templateDir/link already exists as a symlink outside the templates tree, the later CreateFolder/WriteFile path will still follow it and escape the containment boundary. Please walk up to the first existing parent before calling IsPathWithinDirectory, and add a regression test for a symlinked child directory.

🤖 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/installer/template.go` around lines 281 - 283, The path containment check
currently calls filepathutil.IsPathWithinDirectory on newPath and
filepath.Dir(newPath), but both can be non-existent and miss a symlinked
ancestor; change the check to walk upward from newPath to the nearest existing
ancestor (using os.Stat or similar) and call filepathutil.IsPathWithinDirectory
against that existing parent and templateDir before proceeding; update the code
paths that call CreateFolder/WriteFile to rely on this new validation and add a
regression test that creates a symlinked child directory (e.g., templateDir/link
-> /outside) and asserts that operations like CreateFolder/WriteFile are
rejected.

Several filesystem trust boundaries relied on
lexical prefix checks to decide whether a path
fell under an allowed directory. That let sibling
paths be treated as if they were children of
trusted directories.

Replace those checks with canonical path
containment checks for helper payload loading,
template archive extraction, custom template
metadata, template path classification, and
headless screenshot output validation.

Signed-off-by: Dwi Siswanto <git@dw1.io>
@dwisiswant0
dwisiswant0 force-pushed the dwisiswant0/refactor/use-path-aware-filesystem-containment-checks branch from fd9bb10 to 85574ef Compare May 22, 2026 22:29

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
pkg/utils/filepath/filepath_test.go (1)

71-75: ⚡ Quick win

Consider adding a test for sibling directory prefix bypass.

The primary vulnerability this PR addresses—sibling paths like /trusted-dir-evil incorrectly matching /trusted-dir with lexical prefix checks—isn't directly tested in this utility's test file. While downstream tests may cover this, adding a unit test here would document the security invariant at the source.

Suggested test case
+func TestIsPathWithinDirectory_SiblingPrefix(t *testing.T) {
+	baseDir := t.TempDir() // e.g., /tmp/TestXXX123
+	siblingDir := baseDir + "-evil"
+	if err := os.MkdirAll(siblingDir, 0o755); err != nil {
+		t.Fatal(err)
+	}
+	siblingFile := filepath.Join(siblingDir, "payload.txt")
+	if err := os.WriteFile(siblingFile, []byte("malicious"), 0o600); err != nil {
+		t.Fatal(err)
+	}
+
+	if IsPathWithinDirectory(siblingFile, baseDir) {
+		t.Fatalf("sibling path %q should NOT be inside %q", siblingFile, baseDir)
+	}
+}
🤖 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/utils/filepath/filepath_test.go` around lines 71 - 75, Add a unit test in
filepath_test.go that asserts IsPathWithinDirectory correctly rejects
sibling-directory prefix bypasses: construct a realDir (e.g., "/trusted-dir")
and an aliasDir or test path that is a lexical sibling prefix (e.g.,
"/trusted-dir-evil" or filepath.Join(realDir+"-evil", "file.js")) and verify
IsPathWithinDirectory returns false; reference the existing test variables/flow
around IsPathWithinDirectory, aliasDir, realDir and missingChildFile to place
the new case nearby so the security invariant is documented at the source.
🤖 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.

Nitpick comments:
In `@pkg/utils/filepath/filepath_test.go`:
- Around line 71-75: Add a unit test in filepath_test.go that asserts
IsPathWithinDirectory correctly rejects sibling-directory prefix bypasses:
construct a realDir (e.g., "/trusted-dir") and an aliasDir or test path that is
a lexical sibling prefix (e.g., "/trusted-dir-evil" or
filepath.Join(realDir+"-evil", "file.js")) and verify IsPathWithinDirectory
returns false; reference the existing test variables/flow around
IsPathWithinDirectory, aliasDir, realDir and missingChildFile to place the new
case nearby so the security invariant is documented at the source.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b926836-ecdf-428b-8ace-812853318b7c

📥 Commits

Reviewing files that changed from the base of the PR and between fd9bb10 and 85574ef.

📒 Files selected for processing (15)
  • pkg/catalog/config/nucleiconfig.go
  • pkg/catalog/config/nucleiconfig_test.go
  • pkg/catalog/config/template.go
  • pkg/catalog/config/template_test.go
  • pkg/installer/template.go
  • pkg/installer/template_test.go
  • pkg/installer/zipslip_unix_test.go
  • pkg/protocols/headless/engine/page_actions.go
  • pkg/protocols/headless/engine/page_actions_test.go
  • pkg/types/types.go
  • pkg/types/types_test.go
  • pkg/utils/filepath/filepath.go
  • pkg/utils/filepath/filepath_test.go
  • pkg/utils/template_path.go
  • pkg/utils/template_path_test.go

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/external/customtemplates/azure_blob.go (1)

92-101: ⚡ Quick win

Add Azure regression coverage for unsafe and nested blob names.

This join is now a filesystem trust boundary, but the supplied tests only cover the GitLab helpers. A small Azure-focused table test here would lock in traversal rejection and nested-path preservation for blob names too.

🤖 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/external/customtemplates/azure_blob.go` around lines 92 - 101, Add a
table-driven unit test exercising Azure blob name handling to ensure
safeJoinWithinDirectory and the downloadTemplate flow reject path-traversal
names and preserve nested paths: create cases for an unsafe blob name like
"../evil.txt" that should be rejected (assert error/log and no file created) and
a nested blob name like "dir/sub/template.txt" that should be accepted and
written under the configured download directory (assert resolved path is inside
download dir and file content saved). Use the same Azure helper setup used by
existing GitLab tests but target the functions safeJoinWithinDirectory and
downloadTemplate (via the blob-processing code path that calls them) so the
Azure branch in azure_blob.go is covered. Ensure assertions check both
error/no-error and resulting filesystem paths, and add the table test to the
package tests alongside the other helper tests.
🤖 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/external/customtemplates/gitlab.go`:
- Around line 91-99: The per-project output path currently uses project.Path
(the repo slug) which can collide across namespaces; update the call sites that
compute projectOutputPath to use project.PathWithNamespace instead (i.e., pass
project.PathWithNamespace into safeProjectOutputPath and any directory-key
generation) so each project's directory is namespaced and cannot clobber another
project with the same slug; keep the existing containment validation
(safeProjectOutputPath) and MkdirAll usage unchanged, only replace project.Path
with project.PathWithNamespace where the per-project output directory is
constructed.

In `@pkg/protocols/headless/engine/page_actions_test.go`:
- Around line 285-306: The test TestFilesInputAndScreenshotShareLfaGate only
asserts protocolstate.IsLfaAllowed(opts) but never exercises the actual call
sites; update the test to also call page.isScreenshotPathAllowed (or the
exported equivalent used for screenshots) and to dispatch ActionFilesInput
through the page_actions dispatch so those code paths run under the same runtime
override; keep opts.AllowLocalFileAccess false, set
protocolstate.LfaAllowed.Set(executionId, true) as you already do, and assert
that both isScreenshotPathAllowed(...) and the ActionFilesInput dispatch behave
as allowed when the runtime override is present; ensure the existing cleanup of
protocolstate.LfaAllowed.Delete(executionId) remains.

---

Nitpick comments:
In `@pkg/external/customtemplates/azure_blob.go`:
- Around line 92-101: Add a table-driven unit test exercising Azure blob name
handling to ensure safeJoinWithinDirectory and the downloadTemplate flow reject
path-traversal names and preserve nested paths: create cases for an unsafe blob
name like "../evil.txt" that should be rejected (assert error/log and no file
created) and a nested blob name like "dir/sub/template.txt" that should be
accepted and written under the configured download directory (assert resolved
path is inside download dir and file content saved). Use the same Azure helper
setup used by existing GitLab tests but target the functions
safeJoinWithinDirectory and downloadTemplate (via the blob-processing code path
that calls them) so the Azure branch in azure_blob.go is covered. Ensure
assertions check both error/no-error and resulting filesystem paths, and add the
table test to the package tests alongside the other helper tests.
🪄 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: 2fab689a-3645-4095-bb3c-d5853d4bcebd

📥 Commits

Reviewing files that changed from the base of the PR and between 85574ef and 7f52ee3.

📒 Files selected for processing (16)
  • pkg/catalog/disk/find.go
  • pkg/catalog/disk/find_test.go
  • pkg/catalog/disk/path.go
  • pkg/external/customtemplates/azure_blob.go
  • pkg/external/customtemplates/gitlab.go
  • pkg/external/customtemplates/gitlab_test.go
  • pkg/external/customtemplates/s3.go
  • pkg/external/customtemplates/s3_test.go
  • pkg/installer/template.go
  • pkg/installer/zipslip_unix_test.go
  • pkg/protocols/headless/engine/page_actions.go
  • pkg/protocols/headless/engine/page_actions_test.go
  • pkg/reporting/exporters/markdown/markdown.go
  • pkg/reporting/exporters/markdown/markdown_test.go
  • pkg/utils/filepath/filepath.go
  • pkg/utils/filepath/filepath_test.go
💤 Files with no reviewable changes (1)
  • pkg/catalog/disk/path.go

Comment thread pkg/external/customtemplates/gitlab.go
Comment on lines +285 to +306
func TestFilesInputAndScreenshotShareLfaGate(t *testing.T) {
executionId := t.Name()
t.Cleanup(func() {
protocolstate.LfaAllowed.Delete(executionId)
})

opts := &types.Options{ExecutionId: executionId, AllowLocalFileAccess: false}

// Sanity: with no override, both gates must report deny.
require.False(t, protocolstate.IsLfaAllowed(opts),
"baseline IsLfaAllowed should be false when nothing is configured")

// Configure a runtime override via the LfaAllowed map without touching
// opts.AllowLocalFileAccess.
require.NoError(t, protocolstate.LfaAllowed.Set(executionId, true))
require.True(t, protocolstate.IsLfaAllowed(opts),
"IsLfaAllowed must honour the LfaAllowed runtime override")

// The FilesInput dispatch in page_actions.go now calls IsLfaAllowed (the
// same predicate Screenshot uses), so the runtime override is respected
// without the caller having to also flip Options.AllowLocalFileAccess.
}

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test name/intent doesn’t match what is asserted.

This test currently validates only protocolstate.IsLfaAllowed(opts). It never exercises page.isScreenshotPathAllowed or ActionFilesInput dispatch, so it can pass even if those call sites regress.

Suggested tightening
 func TestFilesInputAndScreenshotShareLfaGate(t *testing.T) {
 	executionId := t.Name()
 	t.Cleanup(func() {
 		protocolstate.LfaAllowed.Delete(executionId)
 	})

 	opts := &types.Options{ExecutionId: executionId, AllowLocalFileAccess: false}
+	page := &Page{options: &Options{Options: opts}}

 	// Sanity: with no override, both gates must report deny.
 	require.False(t, protocolstate.IsLfaAllowed(opts),
 		"baseline IsLfaAllowed should be false when nothing is configured")

+	tmpDir := t.TempDir()
+	cwd := filepath.Join(tmpDir, "work")
+	outside := filepath.Join(tmpDir, "outside", "test.png")
+	require.NoError(t, os.MkdirAll(cwd, 0700))
+	require.NoError(t, os.MkdirAll(filepath.Dir(outside), 0700))
+	originalWd, err := os.Getwd()
+	require.NoError(t, err)
+	require.NoError(t, os.Chdir(cwd))
+	t.Cleanup(func() { require.NoError(t, os.Chdir(originalWd)) })
+
+	require.ErrorIs(t, page.isScreenshotPathAllowed(outside), ErrLFAccessDenied)
+
 	// Configure a runtime override via the LfaAllowed map without touching
 	// opts.AllowLocalFileAccess.
 	require.NoError(t, protocolstate.LfaAllowed.Set(executionId, true))
 	require.True(t, protocolstate.IsLfaAllowed(opts),
 		"IsLfaAllowed must honour the LfaAllowed runtime override")
+
+	require.NoError(t, page.isScreenshotPathAllowed(outside))
 }
🤖 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/headless/engine/page_actions_test.go` around lines 285 - 306,
The test TestFilesInputAndScreenshotShareLfaGate only asserts
protocolstate.IsLfaAllowed(opts) but never exercises the actual call sites;
update the test to also call page.isScreenshotPathAllowed (or the exported
equivalent used for screenshots) and to dispatch ActionFilesInput through the
page_actions dispatch so those code paths run under the same runtime override;
keep opts.AllowLocalFileAccess false, set
protocolstate.LfaAllowed.Set(executionId, true) as you already do, and assert
that both isScreenshotPathAllowed(...) and the ActionFilesInput dispatch behave
as allowed when the runtime override is present; ensure the existing cleanup of
protocolstate.LfaAllowed.Delete(executionId) remains.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
pkg/protocols/headless/engine/page_actions_test.go (1)

284-329: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test intent still overstates ActionFilesInput coverage.

This test validates protocolstate.IsLfaAllowed and page.isScreenshotPathAllowed, but it still does not execute the ExecuteActions branch for ActionFilesInput, so a dispatch regression there can pass unnoticed.

🤖 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/headless/engine/page_actions_test.go` around lines 284 - 329,
The test TestFilesInputAndScreenshotShareLfaGate currently checks
protocolstate.IsLfaAllowed and page.isScreenshotPathAllowed but never exercises
the ExecuteActions dispatch for ActionFilesInput; update the test to also call
ExecuteActions (the function that handles ActionFilesInput in page_actions.go)
with an ActionFilesInput targeting outsideTarget and assert it errors with
ErrLFAccessDenied before setting protocolstate.LfaAllowed.Set(executionId,
true), then assert ExecuteActions succeeds after the runtime override so the
ExecuteActions branch is covered and any regression in ActionFilesInput gating
is detected.
🤖 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/common/protocolstate/state.go`:
- Around line 58-71: The Dialers initialization and Init refresh path are
inconsistent: initDialers() must set Dialers.RestrictLocalNetworkAccess and
build Dialers.NetworkPolicy based on options.RestrictLocalNetworkAccess so
IsRestrictLocalNetworkAccess() reflects the option; likewise, in the Init()
refresh branch (where GetDialersWithId(options.ExecutionId) returns
existingDialers) update existingDialers.RestrictLocalNetworkAccess and also
recreate or update existingDialers.NetworkPolicy to reflect the new
RestrictLocalNetworkAccess value (and then call SetLfaAllowed(options) as
before) so deny-list enforcement matches the refreshed setting.

---

Duplicate comments:
In `@pkg/protocols/headless/engine/page_actions_test.go`:
- Around line 284-329: The test TestFilesInputAndScreenshotShareLfaGate
currently checks protocolstate.IsLfaAllowed and page.isScreenshotPathAllowed but
never exercises the ExecuteActions dispatch for ActionFilesInput; update the
test to also call ExecuteActions (the function that handles ActionFilesInput in
page_actions.go) with an ActionFilesInput targeting outsideTarget and assert it
errors with ErrLFAccessDenied before setting
protocolstate.LfaAllowed.Set(executionId, true), then assert ExecuteActions
succeeds after the runtime override so the ExecuteActions branch is covered and
any regression in ActionFilesInput gating is detected.
🪄 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: 7c16708b-8d02-4423-80a9-502777034170

📥 Commits

Reviewing files that changed from the base of the PR and between 7f52ee3 and ccc35a5.

📒 Files selected for processing (8)
  • pkg/external/customtemplates/azure_blob_test.go
  • pkg/external/customtemplates/gitlab.go
  • pkg/installer/zipslip_unix_test.go
  • pkg/protocols/common/protocolstate/state.go
  • pkg/protocols/headless/engine/page_actions.go
  • pkg/protocols/headless/engine/page_actions_test.go
  • pkg/types/types.go
  • pkg/types/types_test.go

Comment on lines +58 to 71
if existingDialers := GetDialersWithId(options.ExecutionId); existingDialers != nil {
// Dialers already exist for this ExecutionId. Refresh the LFA /
// network-policy state derived from options so that a second
// Init call with different options (e.g. flipping
// AllowLocalFileAccess) is reflected in IsLfaAllowed and the
// per-execution dialer state. Without this refresh the second
// caller silently keeps the first caller's settings, which is a
// footgun for tests and SDK callers that share an execution id.
existingDialers.Lock()
existingDialers.LocalFileAccessAllowed = options.AllowLocalFileAccess
existingDialers.RestrictLocalNetworkAccess = options.RestrictLocalNetworkAccess
existingDialers.Unlock()
SetLfaAllowed(options)
return nil

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if RestrictLocalNetworkAccess is read from the Dialers struct elsewhere
echo "=== Searching for reads of RestrictLocalNetworkAccess from Dialers ==="
rg -n --type=go 'RestrictLocalNetworkAccess' -C 3

echo ""
echo "=== Checking Dialers struct definition ==="
ast-grep --pattern 'type Dialers struct {
  $$$
}'

Repository: projectdiscovery/nuclei

Length of output: 7985


Fix RestrictLocalNetworkAccess initialization/refresh inconsistency

  • initDialers() creates Dialers without setting RestrictLocalNetworkAccess, so IsRestrictLocalNetworkAccess() (reads dialers.RestrictLocalNetworkAccess) will return false even when options.RestrictLocalNetworkAccess is true.
  • The Init() refresh path updates existingDialers.RestrictLocalNetworkAccess, but it does not recreate existingDialers.NetworkPolicy, so the deny-list enforcement won’t reflect a changed RestrictLocalNetworkAccess on subsequent Init calls for the same ExecutionId.
Add consistent initialization
 dialersInstance := &Dialers{
     Fastdialer:             dialer,
     NetworkPolicy:          networkPolicy,
     HTTPClientPool:         httpClientPool,
     LocalFileAccessAllowed: options.AllowLocalFileAccess,
+    RestrictLocalNetworkAccess: options.RestrictLocalNetworkAccess,
 }
🤖 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/common/protocolstate/state.go` around lines 58 - 71, The
Dialers initialization and Init refresh path are inconsistent: initDialers()
must set Dialers.RestrictLocalNetworkAccess and build Dialers.NetworkPolicy
based on options.RestrictLocalNetworkAccess so IsRestrictLocalNetworkAccess()
reflects the option; likewise, in the Init() refresh branch (where
GetDialersWithId(options.ExecutionId) returns existingDialers) update
existingDialers.RestrictLocalNetworkAccess and also recreate or update
existingDialers.NetworkPolicy to reflect the new RestrictLocalNetworkAccess
value (and then call SetLfaAllowed(options) as before) so deny-list enforcement
matches the refreshed setting.

@ehsandeep
ehsandeep merged commit 20cbf0b into dev Jun 4, 2026
16 of 19 checks passed
@ehsandeep
ehsandeep deleted the dwisiswant0/refactor/use-path-aware-filesystem-containment-checks branch June 4, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants