* Validates a file system path, handling tilde expansion.
( filePath: string, cwd: string, toolPermissionContext: ToolPermissionContext, operationType: FileOperationType, )
| 1011 | * Validates a file system path, handling tilde expansion. |
| 1012 | */ |
| 1013 | function validatePath( |
| 1014 | filePath: string, |
| 1015 | cwd: string, |
| 1016 | toolPermissionContext: ToolPermissionContext, |
| 1017 | operationType: FileOperationType, |
| 1018 | ): ResolvedPathCheckResult { |
| 1019 | // Remove surrounding quotes if present |
| 1020 | const cleanPath = expandTilde(filePath.replace(/^['"]|['"]$/g, '')) |
| 1021 | |
| 1022 | // SECURITY: PowerShell Core normalizes backslashes to forward slashes on all |
| 1023 | // platforms, but path.resolve on Linux/Mac treats them as literal characters. |
| 1024 | // Normalize before resolution so traversal patterns like dir\..\..\etc\shadow |
| 1025 | // are correctly detected. |
| 1026 | const normalizedPath = cleanPath.replace(/\\/g, '/') |
| 1027 | |
| 1028 | // SECURITY: Backtick (`) is PowerShell's escape character. It is a no-op in |
| 1029 | // many positions (e.g., `/ === /) but defeats Node.js path checks like |
| 1030 | // isAbsolute(). Redirection targets use raw .Extent.Text which preserves |
| 1031 | // backtick escapes. Treat any path containing a backtick as unvalidatable. |
| 1032 | if (normalizedPath.includes('`')) { |
| 1033 | // Red-team P3: backtick is already resolved for StringConstant args |
| 1034 | // (parser uses .value); this guard primarily fires for redirection |
| 1035 | // targets which use raw .Extent.Text. Strip is a no-op for most special |
| 1036 | // escapes (`n → n) but that's fine — wrong guess → no deny match → |
| 1037 | // falls to ask. |
| 1038 | const backtickStripped = normalizedPath.replace(/`/g, '') |
| 1039 | const denyHit = checkDenyRuleForGuessedPath( |
| 1040 | backtickStripped, |
| 1041 | cwd, |
| 1042 | toolPermissionContext, |
| 1043 | operationType, |
| 1044 | ) |
| 1045 | if (denyHit) { |
| 1046 | return { |
| 1047 | allowed: false, |
| 1048 | resolvedPath: denyHit.resolvedPath, |
| 1049 | decisionReason: { type: 'rule', rule: denyHit.rule }, |
| 1050 | } |
| 1051 | } |
| 1052 | return { |
| 1053 | allowed: false, |
| 1054 | resolvedPath: normalizedPath, |
| 1055 | decisionReason: { |
| 1056 | type: 'other', |
| 1057 | reason: |
| 1058 | 'Backtick escape characters in paths cannot be statically validated and require manual approval', |
| 1059 | }, |
| 1060 | } |
| 1061 | } |
| 1062 | |
| 1063 | // SECURITY: Block module-qualified provider paths. PowerShell allows |
| 1064 | // `Microsoft.PowerShell.Core\FileSystem::/etc/passwd` which resolves to |
| 1065 | // `/etc/passwd` via the FileSystem provider. The `::` is the provider |
| 1066 | // path separator and doesn't match the simple `^[a-z]{2,}:` regex. |
| 1067 | if (normalizedPath.includes('::')) { |
| 1068 | // Strip everything up to and including the first :: — handles both |
| 1069 | // FileSystem::/path and Microsoft.PowerShell.Core\FileSystem::/path. |
| 1070 | // Double-:: (Foo::Bar::/x) strips first only → 'Bar::/x' → resolve |
no test coverage detected