Skip to content

fix: prevent symlink following in file protocol scanning#7222

Closed
sandiyochristan wants to merge 2 commits intoprojectdiscovery:devfrom
sandiyochristan:fix/prevent-symlink-following-in-file-protocol
Closed

fix: prevent symlink following in file protocol scanning#7222
sandiyochristan wants to merge 2 commits intoprojectdiscovery:devfrom
sandiyochristan:fix/prevent-symlink-following-in-file-protocol

Conversation

@sandiyochristan
Copy link
Contributor

@sandiyochristan sandiyochristan commented Mar 14, 2026

Proposed Changes

The file protocol scanner follows symlinks when enumerating files, allowing information disclosure of files outside the intended scan scope (CWE-59: Improper Link Resolution Before File Access).

Root Cause

In pkg/protocols/file/find.go, the findFileMatches function uses os.Stat() (line 70) which transparently follows symlinks. When a user scans a directory like /var/www/, an attacker who can write to that directory can place symlinks pointing to sensitive files:

# Attacker creates symlinks in a web directory being scanned
ln -s /etc/shadow /var/www/uploads/shadow
ln -s /home/user/.ssh/id_rsa /var/www/uploads/key
ln -s /etc/ /var/www/uploads/etc_link  # symlink to entire directory

When nuclei scans /var/www/ with the file protocol, os.Stat() follows these symlinks and nuclei processes the target files as if they were inside the scan directory.

Attack Scenarios

Scenario 1: Shared hosting environment

  • Nuclei is used to scan web roots for sensitive file patterns (e.g., .env, credentials.json)
  • Attacker creates symlinks in their web root pointing to other tenants' sensitive files
  • Nuclei follows the symlinks and reports/extracts content from files outside the scan scope

Scenario 2: CI/CD pipeline scanning

  • Repository contains symlinks created by an attacker (via PR)
  • CI pipeline runs nuclei file protocol templates against the checked-out repo
  • Symlinks point to CI secrets, environment files, or host filesystem paths
  • Nuclei follows them and the extracted data appears in scan results

The Fix

findFileMatches — Changed os.Stat() to os.Lstat() which does NOT follow symlinks, and added an explicit check for os.ModeSymlink to skip symlinked files:

info, err := os.Lstat(absPath)  // Lstat: does not follow symlinks
if err != nil {
    return false, err
}
if info.Mode()&os.ModeSymlink != 0 {
    return false, nil  // Skip symlinks
}

findDirectoryMatches — Added d.Type()&os.ModeSymlink check in the filepath.WalkDir callback to skip symlinked entries during recursive directory traversal:

if d.Type()&os.ModeSymlink != 0 {
    return nil  // Skip symlinks
}

Note: filepath.WalkDir already uses os.Lstat internally (per Go docs), so d.Type() correctly reports symlinks. The explicit check ensures they are skipped rather than processed.

Files Changed

  • pkg/protocols/file/find.goos.Statos.Lstat in findFileMatches, symlink skip checks in both findFileMatches and findDirectoryMatches

Security Impact

  • CWE-59: Improper Link Resolution Before File Access
  • CWE-61: UNIX Symbolic Link Following
  • Severity: Medium-High
  • Attack Vector: Attacker places symlinks in a directory scanned by nuclei's file protocol → sensitive files outside scan scope are read and reported
  • Impact: Information disclosure of arbitrary files readable by the nuclei process

Proof

  • os.Lstat is the standard Go approach for symlink-safe file operations — it returns symlink info without resolving the target
  • os.ModeSymlink bit check is the idiomatic Go pattern for detecting symlinks
  • filepath.WalkDir already provides symlink information via d.Type(), so the check has zero performance overhead

Checklist

  • PR created against dev branch
  • All checks passed (lint, unit/integration/regression tests)
  • Minimal, focused change — only file discovery functions modified
  • No behavioral change for regular files — only symlinks are now skipped
  • Consistent with Go standard library symlink handling patterns

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened file-scanning to detect and ignore symbolic links during globbing, file checks, and directory traversal, preventing traversal outside the intended scan scope and improving safety.
    • Preserves existing path validation and public interfaces while excluding symlink targets early in the processing flow.
    • Manifest updated to reflect these changes.

Use os.Lstat instead of os.Stat and check for symlink mode bits
to prevent the file protocol from following symlinks outside the
intended scan scope. This prevents information disclosure when
scanning directories containing attacker-controlled symlinks.
@auto-assign auto-assign bot requested a review from dogancanbakir March 14, 2026 23:01
@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Mar 14, 2026

Neo - PR Security Review

No security issues found

Highlights

  • Fixes symlink following vulnerability (CWE-59) in file protocol's findFileMatches, findDirectoryMatches, and findGlobPathMatches functions
  • Changes os.Stat() to os.Lstat() and adds explicit os.ModeSymlink checks to prevent directory traversal via symlinks
  • Protects against information disclosure attacks where attackers place symlinks in scan directories pointing to sensitive files outside scan scope
Hardening Notes
  • Consider adding a --follow-symlinks flag for legitimate use cases where users want to intentionally follow symlinks during scans
  • Add integration tests with actual symlink scenarios to verify the fix works across different platforms (Linux, macOS, Windows)
  • Document the symlink behavior in the file protocol documentation so users understand that symlinks are now skipped by default

Comment @pdneo help for available commands. · Open in Neo

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

Walkthrough

This change adds symlink guards in file discovery: os.Stat calls are replaced with os.Lstat, and directory/file traversal logic skips symlink entries to avoid following symlink targets outside the scan scope.

Changes

Cohort / File(s) Summary
Symlink guards
pkg/protocols/file/find.go
Replaced os.Stat with os.Lstat in file matching, added early-return for symlinked files, and skip logic for symlink entries during directory traversal and glob matching to prevent following symlinks.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I hop the paths both near and wide,
I sniff for links that try to hide,
With careful paws I skip the trails,
No stray shortcuts, no loose sails,
Safe scans ahead — a tidy stride.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically describes the main security fix: preventing symlink following in file protocol scanning, which directly matches the core objective of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

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

⚠️ Outside diff range comments (1)
pkg/protocols/file/find.go (1)

50-65: ⚠️ Potential issue | 🟠 Major

Missing symlink check in glob matching creates inconsistent security posture.

The findGlobPathMatches function lacks symlink protection, unlike findFileMatches and findDirectoryMatches. An attacker could create a symlink matching a glob pattern (e.g., *.txt/etc/shadow), bypassing the security fix.

Consider adding an os.Lstat check before processing each match:

🛡️ Proposed fix to add symlink protection
 func (request *Request) findGlobPathMatches(absPath string, processed map[string]struct{}, callback func(string)) error {
 	matches, err := filepath.Glob(absPath)
 	if err != nil {
 		return errors.Errorf("wildcard found, but unable to glob: %s\n", err)
 	}
 	for _, match := range matches {
+		// Skip symlinks to prevent traversal outside scan scope
+		info, err := os.Lstat(match)
+		if err != nil {
+			continue
+		}
+		if info.Mode()&os.ModeSymlink != 0 {
+			continue
+		}
 		if !request.validatePath(absPath, match, false) {
 			continue
 		}
 		if _, ok := processed[match]; !ok {
 			processed[match] = struct{}{}
 			callback(match)
 		}
 	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/protocols/file/find.go` around lines 50 - 65, findGlobPathMatches is
missing the symlink protection present in findFileMatches/findDirectoryMatches:
before accepting each match returned by filepath.Glob, perform an os.Lstat on
the match and skip it if the FileMode indicates a symlink (mode&os.ModeSymlink
!= 0); only after passing the lstat check should you call request.validatePath,
update the processed map, and invoke the callback(match) so symlinked targets
are not followed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@pkg/protocols/file/find.go`:
- Around line 50-65: findGlobPathMatches is missing the symlink protection
present in findFileMatches/findDirectoryMatches: before accepting each match
returned by filepath.Glob, perform an os.Lstat on the match and skip it if the
FileMode indicates a symlink (mode&os.ModeSymlink != 0); only after passing the
lstat check should you call request.validatePath, update the processed map, and
invoke the callback(match) so symlinked targets are not followed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7cadbf8-6a78-4313-b3d0-c75d4ed3a5f4

📥 Commits

Reviewing files that changed from the base of the PR and between 979c867 and b1b8341.

📒 Files selected for processing (1)
  • pkg/protocols/file/find.go

findGlobPathMatches was missing the symlink guard present in
findFileMatches and findDirectoryMatches. A symlink matching a glob
pattern (e.g., *.txt -> /etc/shadow) would bypass the fix.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/protocols/file/find.go (1)

56-63: Symlink guard implementation looks correct.

The use of os.Lstat and the bitwise check info.Mode()&os.ModeSymlink != 0 correctly identifies symlinks without following them.

One minor observation: unlike the denylist skip at line 157 which logs at verbose level, skipped symlinks are silent. Consider adding a gologger.Verbose() message for easier debugging in complex scan scenarios.

♻️ Optional: Add verbose logging for skipped symlinks
 		if info.Mode()&os.ModeSymlink != 0 {
+			gologger.Verbose().Msgf("Skipping symlink: %s\n", match)
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/protocols/file/find.go` around lines 56 - 63, Add a verbose log when a
file is skipped due to being a symlink: inside the symlink guard that uses
os.Lstat and checks info.Mode()&os.ModeSymlink, call gologger.Verbose()
(mirroring the denylist skip behavior) to emit a short message indicating the
path was skipped as a symlink for easier debugging during scans.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/protocols/file/find.go`:
- Around line 56-63: Add a verbose log when a file is skipped due to being a
symlink: inside the symlink guard that uses os.Lstat and checks
info.Mode()&os.ModeSymlink, call gologger.Verbose() (mirroring the denylist skip
behavior) to emit a short message indicating the path was skipped as a symlink
for easier debugging during scans.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e8f3a5a-ad90-4225-b2cb-328e40d3c91e

📥 Commits

Reviewing files that changed from the base of the PR and between b1b8341 and f827266.

📒 Files selected for processing (1)
  • pkg/protocols/file/find.go

Copy link
Member

@dwisiswant0 dwisiswant0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please file an issue before submitting fix patches.

Include reproduction steps as per the template and the exact nuclei command (or custom runner PoC for Nuclei SDK) that triggers unexpected behavior. The reproduction must be based on a realistic execution path that is fully concrete and reproducible.

Thanks!

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.

2 participants