Skip to content

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

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)

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_directory
write_file("C:\project\README.md:hidden_stream", "exfil_data")
# AI reads README.md normally — no hint the ADS exists
read_file("C:\project\README.md") # Returns main file, ADS invisible
# Payload extracted later by knowing the stream name
read_file("C:\project\README.md:hidden_stream") # Returns exfil_data

hasNTFSAlternateDataStream() 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 it

findDangerousUnicodeInPath() blocks 18 specific code points plus the entire Unicode Cf (Format) category:

Code PointNameAttack
U+202ERIGHT-TO-LEFT OVERRIDEExtension spoofing
U+200BZERO WIDTH SPACEHook evasion
U+200C/DZW NON-JOINER/JOINERHook evasion
U+202A/BLTR/RTL EMBEDDINGBidi attack
U+2066-2069Bidi ISOLATE charsBidi rendering
U+FEFFBOM / Zero-width NBSPComparison confusion
U+2028/2029LINE/PARAGRAPH SEPPath parsing
U+00ADSOFT HYPHENInvisible confusion

Severity: High
File: core/engine.go IsPathAllowed()

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

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 block
write_file("C:\Projects\NUL", data) # Silently discards all data
# Returns success but nothing written

isWindowsReservedName() 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

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)
}

As part of this hardening, IsPathAllowed() was refactored to:

  1. Run security checks (validatePathSecurity) first, always
  2. Return true when AllowedPaths is empty (open-access mode) — after security checks pass
  3. 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.


FileChange
core/path_security.goNEWvalidatePathSecurity, ADS/Unicode/reserved name detection
core/engine.goRemoved WSL bypass; refactored IsPathAllowed to always call security checks; removed 6 outer AllowedPaths guards
core/file_operations.goRemoved 7 outer AllowedPaths guards
core/edit_operations.goRemoved 3 outer AllowedPaths guards
core/streaming_operations.goRemoved 1 outer AllowedPaths guard
core/plan_mode.goRemoved 1 outer AllowedPaths guard
core/hooks.goCross-platform cmd /Csh -c; added runtime import
SECURITY.mdFull AI-era threat documentation with PoC examples
CHANGELOG.mdSecurity section with all 5 mitigations