跪拜 Guibai
← All articles
Frontend · Artificial Intelligence · Full Stack

Building a Secure File Reader for a Coding Agent: Five Boundaries That Prevent Path Traversal

By 东方小月 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

LLM-powered coding tools that read local files are a path-traversal disaster waiting to happen. This implementation shows exactly where the checks must go — before `realpath()` to avoid information leaks, and after `realpath()` to catch symlink escapes — and why `startsWith` is never a directory-containment check.

Summary

The di-code coding agent now has a local `read` tool that goes far beyond a simple `readFile()` wrapper. It enforces a strict allowed root directory, prevents `../` and absolute-path escapes, and resolves symbolic links to block realpath traversal. Before returning any content, the tool checks for NUL bytes to reject binary files and applies dual output caps: a maximum line count and a 50 KiB UTF-8 byte budget that never splits a line mid-character.

Every read operation follows a sequenced pipeline: cancel check, parameter validation, lexical path containment, realpath containment, Buffer read, binary detection, UTF-8 line splitting, user-requested offset/limit, and finally the application-level line and byte ceilings. Truncated results include an explicit continuation hint with the correct next offset so the model can paginate without guessing.

Tests cover empty files, trailing newlines, Chinese-character byte counting, symlink escapes, and cancellation via AbortSignal. The tool throws on errors and returns plain `ToolResultContent[]` — error-to-message conversion stays in the Agent Loop, keeping file-system code decoupled from conversation state.

Takeaways
Lexical path containment must run before `realpath()` to prevent leaking whether an external path exists via `ENOENT`.
`realpath()` resolution must be checked a second time to block symlinks inside the allowed root that point outside it.
`startsWith` fails as a directory-containment check: `C:\work-secret` starts with `C:\work` but is not a subdirectory.
UTF-8 byte counting via `Buffer.byteLength` is mandatory for multi-byte text; JavaScript `.length` severely underestimates Chinese content.
Output limits never return a partial line — the tool calculates the byte cost of a full line before appending, and throws if even one line exceeds the cap.
Continuation hints include the exact next `offset` so the model can paginate without recalculating line numbers.
Binary files are rejected by scanning the first 8 KiB for NUL bytes, a pragmatic check that catches most formats without a full file scan.
`AbortSignal` is checked before any work and again after async path resolution, giving clean cancellation without promising mid-read interruption.
The tool throws on errors instead of constructing `isError` messages; the Agent Loop owns error-to-message conversion.
Conclusions

Most "secure file reader" implementations for LLM tools stop at one path check; the double check here — lexical then realpath — closes a symlink escape that a single check misses.

Rejecting a path before `realpath()` isn't just about security — it prevents leaking whether an external file exists, which is an information-disclosure vector many tools ignore.

The decision to count UTF-8 bytes rather than characters is easy to overlook but critical: a 50 KiB cap measured in characters could silently balloon to 150 KiB of actual bytes with CJK text.

Returning a precise next offset in truncation hints shifts pagination logic from the model to the tool, reducing the chance the model invents a wrong line number.

Concepts & terms
Lexical path check
Validating that a resolved file path string sits inside an allowed root directory before any filesystem call, using `path.relative()` rather than string prefix matching to prevent `../` and absolute-path escapes.
Realpath containment
Resolving a file's true location with `fs.realpath()` and checking it against the allowed root a second time, specifically to block symbolic links that point outside the root even when the link's own path appears safe.
UTF-8 byte budget
Capping tool output by counting UTF-8 bytes (`Buffer.byteLength`) rather than JavaScript string characters, because multi-byte characters like Chinese can triple the actual byte size relative to `.length`.
NUL byte detection
A lightweight heuristic that scans the first 8 KiB of a file for zero-value bytes to reject binary files before they reach an LLM's text context.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗