Skip to content

fix(smb): bound file reads before buffering - #7624

Open
james-yusuke wants to merge 6 commits into
projectdiscovery:devfrom
james-yusuke:test/fix-smb-read-size-cap
Open

fix(smb): bound file reads before buffering#7624
james-yusuke wants to merge 6 commits into
projectdiscovery:devfrom
james-yusuke:test/fix-smb-read-size-cap

Conversation

@james-yusuke

@james-yusuke james-yusuke commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • stream SMB file reads instead of using the unbounded path
  • stop reading after the configured byte limit is exceeded
  • add regression coverage for bounded reads and unsupported backends

Root cause

The previous implementation checked the size only after the SMB client had buffered the complete file.

Validation

  • ok github.com/projectdiscovery/nuclei/v3/pkg/js/libs/smbsession (cached)
  • ok github.com/projectdiscovery/nuclei/v3/pkg/js/libs/smb (cached)
    ok github.com/projectdiscovery/nuclei/v3/pkg/js/libs/dcerpc (cached)
    ok github.com/projectdiscovery/nuclei/v3/pkg/protocols/file (cached)

Summary by CodeRabbit

  • Bug Fixes
    • Improved SMB file reading to consistently enforce configured size limits.
    • Prevented unsupported backends from attempting unsafe or unavailable read operations.
    • Improved cleanup of opened files and mounted shares after reads.
    • Added clearer error handling when bounded reading is unavailable.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Session.ReadFile now uses bounded reads for injected SMB backends and mounted shares for native clients. Tests verify read limits and unsupported backends. One HTTP integration test now runs serially.

Changes

SMB bounded read flow

Layer / File(s) Summary
Bounded SMB reads and validation
pkg/js/libs/smbsession/session.go, pkg/js/libs/smbsession/session_test.go
Session.ReadFile separates backend and native-client paths. Bounded reads enforce maxBytes, close resources, and reject unsupported backends without calling Cat. Tests verify these behaviors.

HTTP integration test serialization

Layer / File(s) Summary
Serial HTTP integration case
internal/tests/integration/http_test.go
The dsl-matcher-variable.yaml test now runs serially.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant SessionReadFile
  participant Backend
  participant MountedShare

  Caller->>SessionReadFile: ReadFile(path, maxBytes)
  alt Injected backend
    SessionReadFile->>Backend: Open(path)
    Backend-->>SessionReadFile: File handle
  else Native client
    SessionReadFile->>MountedShare: Open(path)
    MountedShare-->>SessionReadFile: File handle
  end
  SessionReadFile->>SessionReadFile: readBoundedFile(maxBytes)
  SessionReadFile-->>Caller: Content or read error
Loading

Suggested reviewers: mzack9999

Poem

A rabbit reads bytes, one bounded hop,
No Cat fallback makes the stream stop.
Shares close cleanly, tests count each chew,
One HTTP case runs serially too.
🐇 Neat little changes, precise and true.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 describes the main change: enforcing bounded SMB file reads before buffering.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@james-yusuke
james-yusuke marked this pull request as ready for review July 31, 2026 05:32

@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

🧹 Nitpick comments (1)
pkg/js/libs/smbsession/session.go (1)

214-241: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared validation logic from readFile and readClientFile.

readFile (Lines 214-227) and readClientFile (Lines 244-256) repeat the same four steps: RequireShareName, maxBytes default, NormalizeSharePath, and the empty-path check. Extract this into one helper that both functions call before they diverge on the connection mechanism. This reduces the risk that a future validation change is applied to only one path.

♻️ Proposed refactor
+func prepareReadFile(share, filePath string, maxBytes int64) (string, int64, error) {
+	if err := RequireShareName(share); err != nil {
+		return "", 0, err
+	}
+	if maxBytes <= 0 {
+		maxBytes = DefaultMaxReadBytes
+	}
+	normalized, err := NormalizeSharePath(filePath)
+	if err != nil {
+		return "", 0, err
+	}
+	if normalized == "." {
+		return "", 0, fmt.Errorf("file path cannot be empty")
+	}
+	return normalized, maxBytes, nil
+}
+
 func readFile(ops shareBackend, share, filePath string, maxBytes int64) (string, error) {
-	if err := RequireShareName(share); err != nil {
-		return "", err
-	}
-	if maxBytes <= 0 {
-		maxBytes = DefaultMaxReadBytes
-	}
-	normalized, err := NormalizeSharePath(filePath)
+	normalized, maxBytes, err := prepareReadFile(share, filePath, maxBytes)
 	if err != nil {
 		return "", err
 	}
-	if normalized == "." {
-		return "", fmt.Errorf("file path cannot be empty")
-	}
 	if err := ops.UseShare(share); err != nil {
 		return "", fmt.Errorf("mount share %q: %w", share, err)
 	}
 	opener, ok := ops.(shareOpener)
 	if !ok {
 		return "", fmt.Errorf("SMB backend does not support bounded file reads")
 	}
 	f, err := opener.Open(normalized)
 	if err != nil {
 		return "", err
 	}
 	defer func() { _ = f.Close() }()
 	return readBoundedFile(f, normalized, maxBytes)
 }
 
 func readClientFile(client *gpsmb.Client, share, filePath string, maxBytes int64) (string, error) {
-	if err := RequireShareName(share); err != nil {
-		return "", err
-	}
-	if maxBytes <= 0 {
-		maxBytes = DefaultMaxReadBytes
-	}
-	normalized, err := NormalizeSharePath(filePath)
+	normalized, maxBytes, err := prepareReadFile(share, filePath, maxBytes)
 	if err != nil {
 		return "", err
 	}
-	if normalized == "." {
-		return "", fmt.Errorf("file path cannot be empty")
-	}
 
 	mountedShare, err := client.Session.Mount(share)

Also applies to: 243-270

🤖 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/js/libs/smbsession/session.go` around lines 214 - 241, Extract the shared
RequireShareName, maxBytes defaulting, NormalizeSharePath, and empty-path
validation from readFile and readClientFile into a helper returning the
validated share, normalized path, and effective limit. Update both functions to
call this helper before their separate backend/connection logic, preserving
existing validation errors and defaults.
🤖 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/js/libs/smbsession/session.go`:
- Around line 243-270: Add coverage for the native-client path by testing
readClientFile through a gpsmb.Client returned by FromClient, using
client.Session.Mount to open a fixture file and exercising readBoundedFile with
limits below and above the file size. Preserve existing validation and error
behavior while asserting bounded content and the relevant read errors.

---

Nitpick comments:
In `@pkg/js/libs/smbsession/session.go`:
- Around line 214-241: Extract the shared RequireShareName, maxBytes defaulting,
NormalizeSharePath, and empty-path validation from readFile and readClientFile
into a helper returning the validated share, normalized path, and effective
limit. Update both functions to call this helper before their separate
backend/connection logic, preserving existing validation errors and defaults.
🪄 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 Plus

Run ID: d0ab2f04-c18d-4d56-a40e-304dbdafab5a

📥 Commits

Reviewing files that changed from the base of the PR and between ba05210 and 32e1508.

📒 Files selected for processing (3)
  • internal/tests/integration/http_test.go
  • pkg/js/libs/smbsession/session.go
  • pkg/js/libs/smbsession/session_test.go

Comment on lines +243 to +270
func readClientFile(client *gpsmb.Client, share, filePath string, maxBytes int64) (string, error) {
if err := RequireShareName(share); err != nil {
return "", err
}
body, err := ops.Cat(normalized)
if maxBytes <= 0 {
maxBytes = DefaultMaxReadBytes
}
normalized, err := NormalizeSharePath(filePath)
if err != nil {
return "", err
}
if normalized == "." {
return "", fmt.Errorf("file path cannot be empty")
}

mountedShare, err := client.Session.Mount(share)
if err != nil {
return "", fmt.Errorf("mount share %q: %w", share, err)
}
defer func() { _ = mountedShare.Umount() }()

f, err := mountedShare.Open(normalized)
if err != nil {
return "", err
}
defer func() { _ = f.Close() }()
return readBoundedFile(f, normalized, maxBytes)
}

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for any test exercising readClientFile or Session{client: ...} in smbsession.
rg -n 'readClientFile|client:\s*&?gpsmb' --type=go -C3

Repository: projectdiscovery/nuclei

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -u

echo "Find smbsession files:"
fd -a 'session(_test)?\.go|.*\.go$' pkg/js/libs/smbsession 2>/dev/null | sed 's#^\./##' || find pkg/js/libs/smbsession -type f -name '*.go'

echo
echo "Session-related tests/files outlines:"
for f in $(fd 'session(_test)?\.go$' pkg/js/libs/smbsession 2>/dev/null); do
  echo "--- $f ($(wc -l < "$f") lines) ---"
  ast-grep outline "$f" 2>/dev/null || true
done

echo
echo "Search read paths in smbsession:"
rg -n 'func read|readClientFile|readFile|readBoundedFile|func.*Read|ReadFile|Read|Mount\(|Open\(' pkg/js/libs/smbsession --type=go -C 2 || true

echo
echo "Search tests/names for bounded/native/client backend in repository:"
rg -n 'TestRead|readClientFile|readFile|readBoundedFile|Bound|Backend|Native|gpsmb\.Client|Session\{[^}]*client|{[^}]*client' --type=go -C 2 || true

echo
echo "Git status/diff stat:"
git diff --stat || true
git diff --name-only || true

Repository: projectdiscovery/nuclei

Length of output: 15875


🏁 Script executed:

#!/bin/bash
set -u

echo "Repository-wide call sites for FromClient/Rfile/native paths:"
rg -n 'FromClient|FromFile|ReadFile\(|readClientFile|readBoundedFile|Native\(\)|LimitReader|DefaultMaxReadBytes' --type=go -C 2 || true

echo
echo "All Go test mentions of read bounded/client/native/fake:"
rg -n 'ReadFile|readFile|readClientFile|readBoundedFile|FromClient|Native|Limit|Oversize|backend' --type=test -C 2 || true

echo
echo "Session tests only relevant lines:"
sed -n '1,120p' pkg/js/libs/smbsession/session_test.go

Repository: projectdiscovery/nuclei

Length of output: 431


Add coverage for readClientFile bounded reads.

The existing session_test.go tests only create Session{backend: ...}, so they exercise readFile without covering the native-client path returned by FromClient. Add tests or integration coverage for readClientFile bounded reads through client.Session.Mount and readBoundedFile.

🤖 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/js/libs/smbsession/session.go` around lines 243 - 270, Add coverage for
the native-client path by testing readClientFile through a gpsmb.Client returned
by FromClient, using client.Session.Mount to open a fixture file and exercising
readBoundedFile with limits below and above the file size. Preserve existing
validation and error behavior while asserting bounded content and the relevant
read errors.

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.

1 participant