Skip to content

chore(policy): harden loadPresetFromFile against oversize and symlinked inputs#3020

Merged
ericksoa merged 3 commits into
mainfrom
chore/policy-preset-hardening
May 5, 2026
Merged

chore(policy): harden loadPresetFromFile against oversize and symlinked inputs#3020
ericksoa merged 3 commits into
mainfrom
chore/policy-preset-hardening

Conversation

@laitingsheng

@laitingsheng laitingsheng commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Three follow-up hardening items from PR #2077 review against loadPresetFromFile in src/lib/policies.ts: a 10 MB file-size guard, symlink rejection via O_NOFOLLOW, and tmp-dir cleanup in the corresponding test block. The size-and-symlink checks now use an atomic openSync(O_RDONLY | O_NOFOLLOW) → fstatSync → readFileSync(fd) pattern (per CodeRabbit review), so there is no TOCTOU window between the stat and the read and the kernel rejects symbolic links at open time.

Related Issue

Closes #2521

Changes

  • Add MAX_PRESET_FILE_BYTES = 10_000_000 and reject files larger than the limit before reading their contents.
  • Replace the prior existsSync + statSync pair with an atomic openSync(O_RDONLY | O_NOFOLLOW) followed by fstatSync on the descriptor and readFileSync(fd, "utf-8"), with a try/finally closeSync on every early-return path. Symbolic links are rejected via ELOOP from openSync and surfaced with a clear error pointing the user at realpath.
  • Track and clean up mkdtempSync directories created by the writeTmp helper inside the loadPresetFromFile describe block via afterEach.
  • Add tests covering the new oversize-file (>10 MB) and symlink-rejection paths.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Verification

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • make docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Tinson Lai tinsonl@nvidia.com

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced preset file validation with strict extension checking and a 10MB file size limit.
    • Improved security by rejecting symbolic links and enforcing stricter file access and permission checks, with clearer error messages for invalid files.
  • Tests

    • Added tests covering preset file size rejection and symbolic link rejection scenarios.

…ed inputs

Adds a 1 MiB stat-size guard and a `lstatSync`-based symlink check to
loadPresetFromFile, so the function rejects unexpectedly large files
before reading and refuses to follow symbolic links. Also collapses the
prior existsSync/statSync pair into a single lstat call wrapped in a
try/catch.

Cleans up the temp directories that the writeTmp helper in
loadPresetFromFile's test block was leaking, and adds tests for the new
size-limit and symlink-reject paths.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented May 5, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1d67494a-1107-41a5-944a-5f7ce55e40aa

📥 Commits

Reviewing files that changed from the base of the PR and between fd6a117 and 4f5f220.

📒 Files selected for processing (1)
  • src/lib/policies.ts

📝 Walkthrough

Walkthrough

loadPresetFromFile now rejects non-.yaml/.yml inputs, opens files with O_NOFOLLOW to refuse symlinks, enforces MAX_PRESET_FILE_BYTES = 10_000_000, verifies the target is a regular file via fstatSync, reads via readSync/fd, and surfaces distinct error messages. Tests add cleanup and cover size and symlink rejections.

Changes

Preset File Validation Hardening

Layer / File(s) Summary
Constant Definition
src/lib/policies.ts
Adds MAX_PRESET_FILE_BYTES = 10_000_000.
Input Validation
src/lib/policies.ts
Pre-checks file extension to allow only .yaml/.yml before any filesystem access.
Safe Open & Stat
src/lib/policies.ts
Opens file with `O_RDONLY
Read and Parse
src/lib/policies.ts
Reads file contents via readSync from the opened fd, closes fd in finally, parses YAML with YAML.parse, and keeps existing post-parse validations (mapping shape, preset.name, network_policies, built-in name collisions).
Tests — Cleanup Wiring
test/policies.test.ts
Introduces tmpDirs tracking and an afterEach hook that recursively removes created temp directories; writeTmp helper updated to register temp dirs.
Tests — New Cases
test/policies.test.ts
Adds tests asserting rejection of oversized preset files (error message containing “too large”) and rejection of preset files that are symbolic links (error message containing “must not be a symbolic link”).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I dug a patch of safety bright,

No symlinks scurry in the night,
Ten million bytes I guard with glee,
Closed the file, parsed YAML free,
Hooray for tidy temp-dir light! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 accurately summarizes the main changes: hardening loadPresetFromFile against oversize and symlinked inputs, which directly reflects the core modifications in the implementation.
Linked Issues check ✅ Passed The PR successfully implements the three primary coding requirements from issue #2521: MAX_PRESET_FILE_BYTES size guard enforcement, atomic open with O_NOFOLLOW to prevent symlinks, and test cleanup via afterEach.
Out of Scope Changes check ✅ Passed All changes are directly scoped to hardening loadPresetFromFile and its tests; no unrelated modifications present.

✏️ 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 chore/policy-preset-hardening

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.

Caution

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

⚠️ Outside diff range comments (1)
src/lib/policies.ts (1)

622-653: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close the TOCTOU gap between metadata checks and file read.

At line 624 you validate with lstatSync(abs), but at line 652 readFileSync(abs, ...) re-resolves the path. A local attacker can replace the file between those steps (symlink swap / target swap), bypassing the earlier checks.

Use fs.openSync(abs, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW) to open once with atomic symlink rejection, then use fs.fstatSync(fd) for metadata and fs.readFileSync(fd, "utf-8") to read from the open descriptor. This eliminates the race window. Ensure proper fd cleanup with try/finally on all early returns.

🤖 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 `@src/lib/policies.ts` around lines 622 - 653, Replace the two-step lstat/read
sequence around fs.lstatSync(abs) + fs.readFileSync(abs, ...) with an atomic
open+fstat+read pattern: open the file once via fs.openSync(abs,
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW) to get fd, call
fs.fstatSync(fd) (use the returned stat in place of stat) to perform
isSymbolicLink/isFile/size checks against MAX_PRESET_FILE_BYTES and keep the
/\.ya?ml$/i extension check on abs, then read the contents from the descriptor
(fs.readFileSync(fd, "utf-8") or appropriate read method). Ensure every
early-return path closes fd in a try/finally so the descriptor is always closed;
handle open errors and map existing console.error messages to the same failures
(use filePath/abs in messages) before parsing with YAML.parse.
🤖 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.

Outside diff comments:
In `@src/lib/policies.ts`:
- Around line 622-653: Replace the two-step lstat/read sequence around
fs.lstatSync(abs) + fs.readFileSync(abs, ...) with an atomic open+fstat+read
pattern: open the file once via fs.openSync(abs, fs.constants.O_RDONLY |
fs.constants.O_NOFOLLOW) to get fd, call fs.fstatSync(fd) (use the returned stat
in place of stat) to perform isSymbolicLink/isFile/size checks against
MAX_PRESET_FILE_BYTES and keep the /\.ya?ml$/i extension check on abs, then read
the contents from the descriptor (fs.readFileSync(fd, "utf-8") or appropriate
read method). Ensure every early-return path closes fd in a try/finally so the
descriptor is always closed; handle open errors and map existing console.error
messages to the same failures (use filePath/abs in messages) before parsing with
YAML.parse.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1e08a33f-d82f-4710-a99e-3bf2818acf02

📥 Commits

Reviewing files that changed from the base of the PR and between 6c07b82 and dccd672.

📒 Files selected for processing (2)
  • src/lib/policies.ts
  • test/policies.test.ts

@laitingsheng laitingsheng marked this pull request as ready for review May 5, 2026 09:08
…loading

Replace the lstat + readFileSync sequence with an atomic
openSync(O_RDONLY | O_NOFOLLOW) followed by fstatSync and readFileSync
on the descriptor. The new shape removes the TOCTOU window between
stat and read, lets the kernel reject symbolic links via ELOOP at open
time instead of a separate isSymbolicLink() check, and closes the
descriptor in a try/finally on every early-return path.

Bump MAX_PRESET_FILE_BYTES from 1 MiB to 10 MB (10_000_000), and adjust
the oversize-file test to write enough padding to exceed the new limit.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>

@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

🤖 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 `@src/lib/policies.ts`:
- Around line 650-663: The current pre-read stat check uses stat.size and
MAX_PRESET_FILE_BYTES but then calls fs.readFileSync(fd, "utf-8") which re-stats
and can read a larger file; replace that unbounded read with a bounded read from
the open descriptor: allocate a Buffer of size Math.min(stat.size,
MAX_PRESET_FILE_BYTES), call fs.readSync(fd, buffer, 0, buffer.length, 0) to
read exactly that many bytes into buffer (ensuring position 0), convert
buffer.toString("utf-8") into content, then pass content to YAML.parse as
before; keep the existing checks and error logging using fd, stat,
MAX_PRESET_FILE_BYTES, content and YAML.parse.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 61aec3db-c475-4071-b694-1b8e44527baf

📥 Commits

Reviewing files that changed from the base of the PR and between dccd672 and fd6a117.

📒 Files selected for processing (2)
  • src/lib/policies.ts
  • test/policies.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/policies.test.ts

Comment thread src/lib/policies.ts
readFileSync(fd, "utf-8") performs its own internal stat at read time
and ignores the size we just checked, so a file that grows between the
fstatSync and the read could exceed MAX_PRESET_FILE_BYTES. Replace the
unbounded readFileSync with a bounded readSync loop into a Buffer of
exactly stat.size bytes, then decode as UTF-8.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>

@ericksoa ericksoa 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.

Reviewed locally against head 4f5f220. The symlink rejection, bounded descriptor read, fd cleanup, and normal preset behavior look correct; targeted policies tests passed.

@ericksoa ericksoa merged commit 6f99488 into main May 5, 2026
19 checks passed
@wscurran wscurran added the chore Build, CI, dependency, or tooling maintenance label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Build, CI, dependency, or tooling maintenance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(policy): hardening follow-ups from custom preset file support (#2039 / #2077)

3 participants