Security #24: AI-Era Attack Surface Hardening
Status: RESOLVED in v4.1.4 Category: Security / Attack Surface Severity: High (WSL bypass, Unicode hook evasion) / Medium (ADS, reserved names) Resolution Date: 2026-04-11
Background
Section titled “Background”MCP filesystem servers operated by AI models face a novel threat model compared to traditional CLI tools: the “user” driving operations is an AI that processes external content (files, web pages, cloned repos). An attacker who can influence that content can potentially influence the AI’s actions. This was analyzed through a threat modeling exercise on v4.1.3 resulting in 5 confirmed attack vectors.
Attack 1 — NTFS Alternate Data Streams (Covert Channel)
Section titled “Attack 1 — NTFS Alternate Data Streams (Covert Channel)”Severity: Medium
File: core/path_security.go (new)
Problem
Section titled “Problem”On Windows NTFS, files can have hidden “alternate data streams” (ADS) accessed via filename:streamname syntax. These streams:
- Are invisible to
list_directory, Windows Explorer, and most AV scanners - Passed
IsPathAllowed()(the path starts with the allowed prefix) - Could store persistent payloads between sessions or exfiltrate data
# Attacker plants hidden payload — completely invisible to list_directorywrite_file("C:\project\README.md:hidden_stream", "exfil_data")
# AI reads README.md normally — no hint the ADS existsread_file("C:\project\README.md") # Returns main file, ADS invisible
# Payload extracted later by knowing the stream nameread_file("C:\project\README.md:hidden_stream") # Returns exfil_datahasNTFSAlternateDataStream() in core/path_security.go detects : after the drive-letter colon. Called unconditionally in validatePathSecurity() → IsPathAllowed(). Windows-only via runtime.GOOS == "windows".
Attack 2 — Unicode Control Characters (RTLO Spoofing + Hook Evasion)
Section titled “Attack 2 — Unicode Control Characters (RTLO Spoofing + Hook Evasion)”Severity: Medium
File: core/path_security.go
Problem A: RTLO Extension Spoofing (U+202E)
Section titled “Problem A: RTLO Extension Spoofing (U+202E)”The RIGHT-TO-LEFT OVERRIDE character reverses text rendering in many UIs. A file named script\u202Etxt.ps1 visually appears as scriptlsp.txt in Windows Explorer (looks harmless) but the operating system sees scripttxt.ps1 — a PowerShell script.
Problem B: Zero-Width Space Hook Evasion (U+200B)
Section titled “Problem B: Zero-Width Space Hook Evasion (U+200B)”A zero-width space inserted into a path creates a file that bypasses hook glob patterns:
# Hook configured: deny writes matching "*.env"write_file(".en\u200Bv", secret_api_key) # Creates .en<ZWS>v# "*.env" hook pattern does NOT match — zero-width space invisible but present# File is fully readable and writable; hooks are completely blind to itfindDangerousUnicodeInPath() blocks 18 specific code points plus the entire Unicode Cf (Format) category:
| Code Point | Name | Attack |
|---|---|---|
| U+202E | RIGHT-TO-LEFT OVERRIDE | Extension spoofing |
| U+200B | ZERO WIDTH SPACE | Hook evasion |
| U+200C/D | ZW NON-JOINER/JOINER | Hook evasion |
| U+202A/B | LTR/RTL EMBEDDING | Bidi attack |
| U+2066-2069 | Bidi ISOLATE chars | Bidi rendering |
| U+FEFF | BOM / Zero-width NBSP | Comparison confusion |
| U+2028/2029 | LINE/PARAGRAPH SEP | Path parsing |
| U+00AD | SOFT HYPHEN | Invisible confusion |
Attack 3 — WSL Blanket Path Bypass
Section titled “Attack 3 — WSL Blanket Path Bypass”Severity: High
File: core/engine.go IsPathAllowed()
Problem
Section titled “Problem”Any path starting with \\wsl.localhost\ or \\wsl$\ unconditionally returned true from IsPathAllowed() — regardless of --allowed-paths configuration:
// Before (VULNERABLE) — in IsPathAllowed():if strings.HasPrefix(lowerPath, `\\wsl.localhost\`) { return true // Bypasses ALL access control}With --allowed-paths C:\MyProject, every WSL path was still fully accessible:
read_file("\\wsl.localhost\Ubuntu\etc\shadow") → ALLOWED (bypass)read_file("\\wsl.localhost\Ubuntu\home\user\.ssh\id_rsa") → ALLOWED (bypass)write_file("\\wsl.localhost\Ubuntu\etc\cron.d\backdoor") → ALLOWED (bypass)An attacker needed only to know the machine had WSL to bypass all access restrictions.
Removed the early-return WSL bypass. WSL paths now fall through to the standard resolvedAllowedPaths containment check. In open-access mode (no --allowed-paths configured), WSL paths remain accessible — the behavior only changes when --allowed-paths is set.
Attack 4 — Windows Reserved Device Names (DoS)
Section titled “Attack 4 — Windows Reserved Device Names (DoS)”Severity: Low-Medium
File: core/path_security.go
Problem
Section titled “Problem”Windows treats CON, NUL, COM1-COM9, LPT1-LPT9 as device references regardless of where they appear in a path. IsPathAllowed("C:\Projects\CON") returned true:
read_file("C:\Projects\CON") # Hangs — waits for console stdin (DoS)read_file("C:\Projects\COM1") # Opens serial port — may blockwrite_file("C:\Projects\NUL", data) # Silently discards all data # Returns success but nothing writtenisWindowsReservedName() checks the path base name (case-insensitive, extension-stripped — NUL.txt is still NUL) against the complete device name list. Applied cross-platform so NUL files created on Linux cannot be moved to Windows.
Attack 5 — Cross-Platform Hook Execution Failure
Section titled “Attack 5 — Cross-Platform Hook Execution Failure”Severity: Low (but silently bypasses security hooks)
File: core/hooks.go
Problem
Section titled “Problem”Hook commands used cmd /C unconditionally on all platforms:
cmd = exec.CommandContext(execCtx, "cmd", "/C", hook.Command)On Linux/macOS, cmd.exe doesn’t exist → command fails with exit code -1. The default for exit code -1 was HookContinue (not HookDeny). Result: all hooks silently do nothing on non-Windows.
Worse: a user who configured failOnError:true blocking hooks would think they were protected, but the hooks silently failed and allowed the operation.
if runtime.GOOS == "windows" { cmd = exec.CommandContext(execCtx, "cmd", "/C", hook.Command)} else { cmd = exec.CommandContext(execCtx, "sh", "-c", hook.Command)}Refactoring: IsPathAllowed() Always-On
Section titled “Refactoring: IsPathAllowed() Always-On”As part of this hardening, IsPathAllowed() was refactored to:
- Run security checks (
validatePathSecurity) first, always - Return
truewhen AllowedPaths is empty (open-access mode) — after security checks pass - Otherwise do normal containment check
All 20 call sites that previously checked if len(AllowedPaths) > 0 before calling IsPathAllowed were simplified to unconditional calls, making the security layer impossible to skip.
Files Changed
Section titled “Files Changed”| File | Change |
|---|---|
core/path_security.go | NEW — validatePathSecurity, ADS/Unicode/reserved name detection |
core/engine.go | Removed WSL bypass; refactored IsPathAllowed to always call security checks; removed 6 outer AllowedPaths guards |
core/file_operations.go | Removed 7 outer AllowedPaths guards |
core/edit_operations.go | Removed 3 outer AllowedPaths guards |
core/streaming_operations.go | Removed 1 outer AllowedPaths guard |
core/plan_mode.go | Removed 1 outer AllowedPaths guard |
core/hooks.go | Cross-platform cmd /C → sh -c; added runtime import |
SECURITY.md | Full AI-era threat documentation with PoC examples |
CHANGELOG.md | Security section with all 5 mitigations |