security: sensitive-file checks can be bypassed through symlink aliases
Description
The file-sensitivity policy blocks direct paths such as .env, credentials.json, and server.pem, but it checks the name supplied by the caller without resolving an existing filesystem link first. The Python FileReadTool, FileWriteTool, and ApplyPatchTool therefore follow a harmless-looking symlink to a sensitive file. The Rust file tools use the same basename-only policy.
For example, with notes.txt -> .env:
FileReadTool(path="notes.txt") -> success, returns the .env contents
FileWriteTool(path="notes.txt") -> success, overwrites .env
ApplyPatchTool(path="notes.txt") -> success, patches .envThis bypasses the policy's promise that sensitive files cannot be accessed through the built-in filesystem tools. allowed_dirs can stop links that leave the allowlisted directory, but the default tools have no directory restriction, so the sensitive-file check still needs to handle aliases itself.
Steps to reproduce
- Create a file named
.envcontaining a sentinel value. - Create a symlink named
notes.txtpointing to that file. - Call
FileReadTool().execute(path=".../notes.txt")(or the write/patch tool). - Observe that the operation succeeds and reaches the
.envtarget.
Expected behavior
Sensitive-file matching should inspect the resolved target for existing paths, so symlink aliases to .env, credentials, private keys, and other protected names are rejected before reading or writing. Nonexistent ordinary paths should keep their current behavior.
Scope
Keep this focused on the shared file-sensitivity boundary and parity between the Python fallback and Rust implementation. Add regression tests for read and write/patch aliases. It does not propose a general directory sandbox or a new dependency. The imported-skill copy boundary is handled separately in #960.
Environment
macOS / Python 3.13.4; reproduced against the current upstream main implementation.
Source: open-jarvis/OpenJarvis