fix(mcp): bound lint-pr-text body file reads#2945
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 5421021 | Commit Preview URL Branch Preview URL |
Jul 04 2026, 07:59 AM |
| const stats = lstatSync(path); | ||
| if (!stats.isFile()) throw new Error(`${label} file must be a regular file: ${path}`); | ||
| if (stats.size > cliTextFileMaxBytes) throw new Error(`${label} file is too large: ${path} (max ${cliTextFileMaxBytes} bytes)`); | ||
| return readFileSync(path, "utf8"); |
There was a problem hiding this comment.
TOCTOU race allows symlink and special-file bypass in readCliTextFile
readCliTextFile checks file type with lstatSync but reads with readFileSync, creating a race window.
Open the file with fs.openSync and O_NOFOLLOW, then fstatSync and read from the file descriptor.
AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.
<file name="packages/gittensory-mcp/bin/gittensory-mcp.js">
<violation number="1" location="packages/gittensory-mcp/bin/gittensory-mcp.js:1493">
<priority>P2</priority>
<title>TOCTOU race allows symlink and special-file bypass in readCliTextFile</title>
<evidence>The readCliTextFile function validates that a path is a regular file and under the size limit using lstatSync, but then calls readFileSync(path, 'utf8') afterwards. An attacker can replace the regular file with a symlink or FIFO between the lstatSync check and the readFileSync call, bypassing the safety checks. This is a classic time-of-check to time-of-use (TOCTOU) race condition.</evidence>
<recommendation>Replace the separate lstatSync and readFileSync calls with a single atomic operation: open the path with fs.openSync using fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW, run fs.fstatSync on the returned file descriptor to verify it is a regular file and under the size limit, then pass the file descriptor to readFileSync (which accepts an fd in Node.js). This eliminates the race window.</recommendation>
</violation>
</file>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2945 +/- ##
=======================================
Coverage 96.02% 96.02%
=======================================
Files 259 259
Lines 28406 28406
Branches 10339 10339
=======================================
Hits 27278 27278
Misses 491 491
Partials 637 637 🚀 New features to boost your workflow:
|
Read --body-file through a single file descriptor (openSync with O_NOFOLLOW, then fstatSync/readFileSync/closeSync on that fd) instead of a path-based stat-then-read pair, so a symlink or special file swapped in between the two calls can no longer bypass the isFile() and size checks.
|
Tip 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 ✅ Gittensory review result - approve/merge recommendedReview updated: 2026-07-04 08:01:52 UTC
✅ Suggested Action - Approve/Merge
Review summary Nits — 4 non-blocking
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
stats.size only proves the file's size at fstatSync time; a regular file can still grow before the subsequent read, so trusting the stat alone left readFileSync unbounded. Read at most cliTextFileMaxBytes + 1 bytes directly from the descriptor and fail if that cap is exceeded. Also cover the slop-risk and issue-slop CLI entry points, which share readCliTextFile with lint-pr-text but had no regression test for the non-regular/oversized rejection paths.
Motivation
lint-pr-textCLI accepted--body-filepaths and usedreadFileSyncafter onlyexistsSync, which allowed special files (FIFOs, symlinks, devices) or very large files to hang or exhaust memory.Description
cliTextFileMaxBytes(1 MiB) limit and importlstatSyncinpackages/gittensory-mcp/bin/gittensory-mcp.jsto enable robust file checks.readCliTextFile(path, label)which validates existence, requires a regular file (stats.isFile()), enforces the size cap, and then reads the file.lint-pr-text,slop-risk, andissue-slopCLI codepaths to usereadCliTextFileinstead of rawreadFileSync, preserving normal functionality for regular files under the cap.test/unit/mcp-cli-lint-pr-text.test.tsthat asserts--body-filerejects directories, symlinks, and oversized files.Testing
git diff --checkto validate whitespace/metadata, which passed.npm run build:mcp, which succeeded.npx vitest run test/unit/mcp-cli-lint-pr-text.test.ts test/unit/mcp-cli-slop-risk.test.ts, and all tests passed.Codex Task