@n0zer0d4y/vulcan-file-ops 1.2.14 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
 
11
11
 
12
+ ## [1.3.0] - 2026-10-04
13
+
14
+ Security hardening release. Upgrading is strongly recommended. See **Breaking Changes** before upgrading.
15
+
16
+ ### Security
17
+
18
+ - **execute_shell: commands could bypass the approved-command list.** Newline-separated commands were not seen by the allowlist check. Commands are now parsed with a conservative, quote-aware parser that accepts only a grammar bash and PowerShell interpret the same way; newlines and control characters, backticks, escaped quotes (`\"`, `\'`), a lone `&`, `( )`/`{ }` grouping and script blocks, heredocs, Unicode quotes and PowerShell's `--%` are rejected. Separators inside quotes are now correctly treated as data.
19
+ - **execute_shell: path validation gaps.** Slash-prefixed absolute paths (`/etc/passwd`, `/Windows/...`) were treated as command switches and never validated; redirection targets written without a space (`>C:\outside\file`) were not validated; paths through symlinks/junctions were checked only lexically (GitHub issue #3). Every operand, option value (`--out=...`, `-C:\x`), redirection target and `cd` target is now validated lexically, after `realpath`, and with physical resolution (symlinks followed before `..`). Relative operands are checked against every directory a `cd` could leave the command in. Arguments with unresolvable variables and PowerShell provider paths (`env:`, `HKLM:`, ...) are refused.
20
+ - **execute_shell: dangerous-command check could be self-approved.** The `requiresApproval` argument let the caller (the AI) bypass the dangerous-pattern check. It is now ignored; the server operator can allow specific commands with `--allow-dangerous-commands`. The over-broad `format` pattern no longer matches `Get-Date -Format`.
21
+ - **make_directory could create directories outside approved folders** through a symlink or junction (GitHub issue #3). Paths are now validated canonically and created segment by segment with a `realpath` check after each segment.
22
+ - **register_directory let the AI widen its own access without user involvement.** Registration now requires user confirmation through the MCP client by default (see `--runtime-registration`). The real path is registered; filesystem roots and the home directory require `allow`.
23
+ - **`.env` in the server's working directory controlled the shell allowlist** and was injected into the environment of executed commands. The working-directory `.env` is no longer read (see `--commands-env-file`).
24
+ - **PDF generation could crash the server.** Any `<img>` that was not an inline `data:` image caused an unhandled promise rejection that terminated the process; corrupt PNG data could also crash it. Images are now sanitized (only `data:image/...` images up to 10 MB are embedded; others are replaced by their alt text), PDF rendering errors are returned as tool errors with a 30 s timeout, and unhandled rejections are logged to stderr instead of terminating the server.
25
+ - **DOCX generation fetched remote image URLs** (server-side HTTP request with the response embedded in the document). Remote images are no longer fetched.
26
+ - **grep_files regular expressions could freeze the server** (catastrophic backtracking). Matching now runs in a worker thread with a 5 s per-file and 30 s per-search budget; regex and glob patterns are limited to 1,000 characters.
27
+ - **Unbounded reads and decompression.** Full-mode text reads are capped at 10 MB (use `head`/`tail`/`range` for larger files), `attach_image` at 10 MB per image and 20 MB per call, and Office/ODF documents are checked for zip bombs (size, entry count, compression ratio, ZIP64) before parsing.
28
+ - **Directory copies dereferenced symlinks**, copying files from outside approved folders into them. Copying a directory that contains symlinks or junctions is now refused.
29
+ - **PDF/DOCX output is written atomically** and never through a symlink, like text files.
30
+ - **Dependencies refreshed** with a 14-day release quarantine (`npm --before`), including `@modelcontextprotocol/sdk` 1.20.0 → 1.30.0 (fixes GHSA-345p-7cg4-v4c7, GHSA-w48q-cv73-mx4w, GHSA-8r9q-7v3j-jr4g), `axios` (via `@turbodocx/html-to-docx`), `@xmldom/xmldom`, `minimatch`, `mammoth` and `officeparser` 5.2 → 7.8 (fixes a `file-type` infinite loop on crafted files). Snyk Open Source runtime findings went from 89 to 0 open; one `pdfjs-dist` advisory is ignored with justification because it is unreachable (PDFs are never parsed with `officeparser`, and the issue requires browser rendering). All packages pass `npm audit signatures`.
31
+ - **Office/ODF spreadsheet expansion is bounded.** `officeparser` 7.x caps ODF "repeated cells" at 1,000,000 cells, so a tiny file that claims billions of cells can no longer expand without limit; its ZIP decompression limits are aligned with the zip-bomb pre-check (200 MB, 10,000 entries).
32
+
33
+ ### Breaking Changes
34
+
35
+ - `register_directory` asks the user to confirm each directory by default. Clients without MCP elicitation support get a refusal; use `--approved-folders`, or start the server with `--runtime-registration allow` to restore the previous behavior.
36
+ - The server no longer reads `.env` from its working directory. Pass `--approved-commands`, or `--commands-env-file <path>` (only `APPROVED_COMMANDS` is read). `APPROVED_COMMANDS` set as a process environment variable is no longer used. `--env-file` is rejected because Node.js reserves it.
37
+ - `execute_shell` rejects commands outside the supported grammar (see Security above), and `requiresApproval` no longer bypasses dangerous-pattern blocking.
38
+ - Node.js 20.19+ (or 22.12+) is required (already required by `jsdom` 27; now declared in `engines`).
39
+ - Copying directories that contain symlinks or junctions is refused.
40
+ - Full-mode reads of text files over 10 MB are refused.
41
+ - `officeparser` 7.x (used for PPTX, XLSX and OpenDocument files) adds about 46 MB to the install, mostly the `tesseract.js` OCR engine it depends on. OCR is disabled and the engine is never loaded, but it is downloaded with the package.
42
+ - `move_file` refuses to overwrite an existing destination (previously it replaced it silently). Use `file_operations` with `onConflict: "overwrite"` to replace files.
43
+ - `attach_image` returns SVG files as markup text and rejects BMP files; only PNG, JPEG, GIF and WebP are returned as images.
44
+ - `grep_files` `glob` without a slash now matches at any depth (`*.md` also matches `docs/guide.md`).
45
+
46
+ ### Added
47
+
48
+ - `--runtime-registration <confirm|allow|deny>`, `--allow-dangerous-commands <cmds>`, `--commands-env-file <path>`.
49
+ - `SECURITY.md` with private vulnerability reporting instructions.
50
+ - GitHub Actions CI: runs the test suite and the build on Windows with Node 22.
51
+ - Dependabot version updates for npm and GitHub Actions with a 14-day cooldown.
52
+ - `engines` field in `package.json` (`^20.19.0 || >=22.12.0`).
53
+ - Regression test suites for the fixes above.
54
+
55
+ ### Changed
56
+
57
+ - Blocked `execute_shell` commands now report errors starting with `Access denied:` (validation, approval, dangerous-pattern and path checks).
58
+ - The `.snyk` policy, which was invalid and suppressed nothing, is replaced by a minimal policy with one justified, time-boxed ignore.
59
+ - Test tooling is pinned to `jest` / `@jest/globals` 30.2.0 and `ts-jest` 29.4.6; newer versions hang on several suites in this project.
60
+ - Documentation corrected: Node.js requirement (was "14 or higher"), README security claims that the 1.3.0 audit disproved, and dated notes on the earlier audit reports in `docs/`.
61
+
62
+ ### Fixed
63
+
64
+ - DOCX generation no longer mangles quoted HTML attributes (inline images and `style` attributes work again).
65
+ - Shell path-validation tests that passed for the wrong reason (missing `workdir`) now assert the actual path denial.
66
+ - `move_file` no longer silently overwrites an existing destination, matching its description; case-only renames (e.g. `readme.md` → `README.md`) now take effect.
67
+ - `read_file` / `read_multiple_files` `tail` mode no longer counts the newline at the end of a file as an extra empty line.
68
+ - `grep_files` `glob` without a slash (e.g. `*.md`) now matches files at any depth, like ripgrep; patterns with a slash still match the path from the search root.
69
+ - `write_multiple_files` reports the size of the written file; for PDF/DOCX this was previously the length of the HTML input.
70
+ - `attach_image` no longer sends SVG or BMP as images (vision models reject them): SVG is returned as markup text and BMP returns an error asking for PNG/JPEG.
71
+ - A destination that resolves outside the allowed directories is reported as "Access denied" instead of "Parent directory does not exist".
72
+ - The `execute_shell` description recommends `;` on Windows, where Windows PowerShell 5.1 does not support `&&` or `||`.
73
+
12
74
  ## [1.2.14] - 2026-05-16
13
75
 
14
76
  ### Fixed
package/README.md CHANGED
@@ -72,7 +72,7 @@ This enhanced implementation provides:
72
72
  This server supports multiple flexible approaches to directory access:
73
73
 
74
74
  1. **Pre-configured Access**: Use `--approved-folders` to specify directories on server start for immediate access
75
- 2. **Runtime Registration**: Users can instruct AI agents to register directories during conversation via `register_directory` tool
75
+ 2. **Runtime Registration**: Users can instruct AI agents to register directories during conversation via `register_directory` tool. By default you are asked to confirm each new directory through your MCP client (see [Runtime Registration Policy](#runtime-registration-policy))
76
76
  3. **MCP Roots Protocol**: Client applications can provide workspace directories dynamically
77
77
  4. **Flexible Permissions**: Combine multiple approaches - start with approved folders, add more at runtime
78
78
  5. **Secure Boundaries**: All operations validate against registered directories regardless of access method
@@ -109,7 +109,7 @@ npm install @n0zer0d4y/vulcan-file-ops
109
109
 
110
110
  ### Prerequisites
111
111
 
112
- **Node.js** (version 14 or higher) must be installed on your system. This provides npm and npx, which are required to run this package.
112
+ **Node.js** (version 20.19 or higher, or 22.12 or higher) must be installed on your system. This provides npm and npx, which are required to run this package.
113
113
 
114
114
  - **Download Node.js**: https://nodejs.org/
115
115
  - **Check installation**: Run `node --version` and `npm --version`
@@ -399,6 +399,28 @@ Or enable individual tools:
399
399
  }
400
400
  ```
401
401
 
402
+ #### Shell Commands
403
+
404
+ `execute_shell` only runs commands whose root command is approved:
405
+
406
+ - `--approved-commands npm,node,git` — comma-separated allowlist of root commands. Every command in a chain (`;`, `&&`, `||`, `|`) must be approved.
407
+ - `--allow-dangerous-commands rm` — approved commands that may also run when they match a dangerous pattern (for example `rm -rf`, `del /s`, `Remove-Item -Recurse`, `sudo`). Without this flag such commands are always blocked; the AI cannot override it.
408
+ - `--commands-env-file <path>` — read `APPROVED_COMMANDS` from a `.env`-format file. Only that key is used and nothing is added to the server's environment. `--approved-commands` takes priority.
409
+
410
+ > **Changed in 1.3.0:** the server no longer reads a `.env` file from its working directory. Use `--approved-commands` or `--commands-env-file`. Do not use `--env-file`: Node.js reserves that flag and would apply the file to the server process itself.
411
+
412
+ > **Note:** `execute_shell` is not a sandbox. An approved interpreter or code-running tool (`node`, `python`, `bash`, `npm`, `git`, `find -exec`, ...) can do anything your user account can do. Only approve commands you are comfortable letting the AI run.
413
+
414
+ #### Runtime Registration Policy
415
+
416
+ `--runtime-registration <confirm|allow|deny>` controls the `register_directory` tool:
417
+
418
+ - `confirm` (default) — you are asked to approve each new directory through your MCP client's confirmation prompt (MCP elicitation). If the client does not support confirmation prompts, registration is refused; add the folder with `--approved-folders` instead, or use `allow`.
419
+ - `allow` — directories are registered without a prompt (the behavior before 1.3.0).
420
+ - `deny` — runtime registration is disabled; only `--approved-folders` and MCP Roots grant access.
421
+
422
+ Filesystem roots (such as `C:\` or `/`) and your home directory itself can only be registered with `allow`.
423
+
402
424
  #### Combined Configuration
403
425
 
404
426
  All configuration options can be combined:
@@ -490,7 +512,7 @@ To access a specific directory, instruct the AI agent:
490
512
  "Please register the directory C:\path\to\your\folder for access, then list its contents."
491
513
  ```
492
514
 
493
- The AI will use the `register_directory` tool to gain access, then perform operations within that directory.
515
+ The AI will use the `register_directory` tool to gain access, then perform operations within that directory. With the default `--runtime-registration confirm` policy your MCP client asks you to approve the directory first.
494
516
 
495
517
  ## API
496
518
 
@@ -526,7 +548,7 @@ Attach images for AI vision analysis
526
548
 
527
549
  - `path` (string | string[]): Path to image file, or array of paths to attach multiple images at once
528
550
 
529
- **Output:** Image content in MCP format for vision model processing. Supports PNG, JPEG, GIF, WebP, BMP, SVG
551
+ **Output:** Image content in MCP format for vision model processing. Supports PNG, JPEG, GIF and WebP (the formats vision models accept). SVG files are returned as their markup text; BMP is not supported (convert to PNG)
530
552
 
531
553
  ##### read_multiple_files
532
554
 
@@ -708,7 +730,7 @@ Enable runtime access to new directories
708
730
 
709
731
  - `path` (string): Directory path to register
710
732
 
711
- **Output:** Success confirmation. Directory becomes accessible for operations
733
+ **Output:** Success confirmation. Directory becomes accessible for operations. The directory's real path (symlinks resolved) is registered. Subject to `--runtime-registration` (default: user confirmation via the MCP client).
712
734
 
713
735
  ##### list_allowed_directories
714
736
 
@@ -730,7 +752,7 @@ Find files using glob pattern matching
730
752
  - `pattern` (string): Glob pattern (e.g., `**/*.ts`)
731
753
  - `excludePatterns` (array, optional): Patterns to exclude
732
754
 
733
- **Output:** List of matching file paths
755
+ **Output:** List of matching file paths. Patterns are limited to 1,000 characters.
734
756
 
735
757
  ##### grep_files
736
758
 
@@ -751,6 +773,8 @@ Search for text patterns within files
751
773
 
752
774
  **Output:** Matching lines with context, file paths, or match counts
753
775
 
776
+ Regex matching runs in a worker thread with a time budget (5 s per file, 30 s per search), so a pathological pattern fails with a timeout error instead of freezing the server. Patterns are limited to 1,000 characters.
777
+
754
778
  #### Shell Operations
755
779
 
756
780
  ##### execute_shell
@@ -763,6 +787,7 @@ Execute shell commands with security controls
763
787
  - `description` (string, optional): Command purpose
764
788
  - `workdir` (string, optional): Working directory (must be within allowed directories). If not provided, process.cwd() is used and validated
765
789
  - `timeout` (number, optional): Timeout in milliseconds (default: 30000)
790
+ - `requiresApproval` (boolean, optional): Deprecated and ignored (kept for compatibility)
766
791
 
767
792
  **Output:** Exit code, stdout, stderr, and execution metadata
768
793
 
@@ -770,8 +795,11 @@ Execute shell commands with security controls
770
795
 
771
796
  - At least one approved directory must be configured before executing shell commands
772
797
  - Working directory (whether explicit or default process.cwd()) is always validated against allowed directories
773
- - All file/directory paths in command arguments are automatically extracted and validated against allowed directories
774
- - Commands referencing paths outside approved directories are blocked, preventing directory restriction bypasses
798
+ - Every command in a chain (`;`, `&&`, `||`, `|`) must be in `--approved-commands`
799
+ - File and directory operands (arguments, option values such as `--out=...`, and redirection targets) are validated against allowed directories after resolving symlinks and junctions; operands with variables that cannot be resolved are refused
800
+ - Commands must be a single line. Not allowed: newlines, command substitution, backticks, a lone `&`, `( )`/`{ }` grouping or script blocks, heredocs, and escaped quotes (`\"`, `\'`)
801
+ - Commands matching dangerous patterns are blocked unless the operator allowed them with `--allow-dangerous-commands`
802
+ - `execute_shell` is not a sandbox: approved interpreters and code-running tools can still do anything your account can do
775
803
 
776
804
  ### Multi-File Edit Examples
777
805
 
@@ -849,7 +877,7 @@ For detailed usage examples, see [Tool Usage Guide](docs/TOOL_USAGE_GUIDE.md)
849
877
 
850
878
  ## Security
851
879
 
852
- This MCP server implements enterprise-grade security controls to protect against common filesystem vulnerabilities. All security measures are based on industry best practices and address known CVE patterns.
880
+ This MCP server implements security controls to protect against common filesystem vulnerabilities, based on known CVE patterns. To report a vulnerability, please follow [SECURITY.md](SECURITY.md) (private reporting; do not open a public issue).
853
881
 
854
882
  ### Protected Against
855
883
 
@@ -863,17 +891,17 @@ This MCP server implements enterprise-grade security controls to protect against
863
891
  #### Command Injection (CWE-78)
864
892
 
865
893
  - **Protected Pattern**: CVE-2025-54795
866
- - **Mitigation**: Multi-layer validation including command substitution detection, root command extraction, and dangerous pattern matching
867
- - **Implementation**: Blocks `$()`, `` ` ` ``, `>()`, `<()` patterns; validates all commands in chains; requires approval for dangerous operations
868
- - **Example**: Prevents `echo "; malicious_cmd; echo"` injection attempts
894
+ - **Mitigation**: Commands are parsed with a conservative, quote-aware parser; only a small grammar that bash and PowerShell interpret the same way is accepted, and every command in a chain must be approved
895
+ - **Implementation**: Rejects newlines and control characters, command substitution, backticks, escaped quotes, a lone `&`, grouping/script blocks and heredocs; dangerous patterns are blocked unless the operator allows them (`--allow-dangerous-commands`)
896
+ - **Example**: `echo "\"; malicious_cmd; echo \""` and newline-separated commands are rejected
869
897
 
870
898
  #### Shell Command Directory Bypass (CWE-22)
871
899
 
872
- - **Protected Pattern**: Path restriction bypass via absolute paths in shell commands
873
- - **Mitigation**: Path extraction and validation for all file/directory paths embedded in command arguments
874
- - **Implementation**: Extracts paths from command strings (handles Windows/Unix paths, quotes, relative paths, environment variables), validates each path against allowed directories before execution
875
- - **Example**: Blocks `type C:\Windows\System32\drivers\etc\hosts` and `cat /etc/passwd` when these paths are outside approved directories
876
- - **Scope**: Applies to all shell commands executed via `execute_shell` tool - paths in arguments are validated just like filesystem operations
900
+ - **Protected Pattern**: Path restriction bypass via paths in shell commands
901
+ - **Mitigation**: Every file operand (arguments, option values such as `--out=...`, redirection targets, `cd` targets) is validated against allowed directories after resolving symlinks and junctions
902
+ - **Implementation**: Operands are checked lexically, after `realpath`, and with physical resolution (symlinks followed before `..`, as a POSIX kernel does); relative operands are checked against every directory a `cd` could leave the command in; unresolvable variables and PowerShell provider paths (`env:`, `HKLM:`, ...) are refused
903
+ - **Example**: Blocks `cat /etc/passwd`, `type C:\Windows\System32\drivers\etc\hosts`, `echo x >C:\outside\file` and `cat link/secret` (where `link` points outside) when the targets are outside approved directories
904
+ - **Scope**: `execute_shell` is not a sandbox. An approved interpreter or code-running tool can still do anything your account can do, so approve commands sparingly
877
905
 
878
906
  #### Symlink Attacks (CWE-59 / CWE-61)
879
907
 
@@ -899,16 +927,17 @@ This MCP server implements enterprise-grade security controls to protect against
899
927
 
900
928
  #### Command Execution
901
929
 
902
- - **Command Whitelisting**: Only pre-approved commands execute without confirmation
903
- - **Pattern Detection**: Blocks dangerous patterns (destructive, privilege escalation, network execution)
904
- - **Command Substitution Blocking**: Prevents `$()`, backticks, process substitution
930
+ - **Command Allowlist**: Only commands listed in `--approved-commands` execute; everything else is blocked
931
+ - **Pattern Detection**: Blocks dangerous patterns (destructive, privilege escalation, network execution) unless the operator allows the command with `--allow-dangerous-commands`
932
+ - **Structural Parsing**: Rejects command substitution, backticks, newlines, grouping and other constructs that could hide commands from validation
905
933
  - **Root Command Extraction**: Analyzes all commands in chained operations for approval
906
- - **Path Argument Validation**: Extracts and validates all file/directory paths in command arguments against allowed directories (prevents bypass via absolute paths in commands)
934
+ - **Path Operand Validation**: Validates all file/directory operands, including redirection targets, against allowed directories with symlink resolution
907
935
 
908
936
  #### Access Controls
909
937
 
910
938
  - **Directory Whitelisting**: Operations restricted to explicitly approved directories
911
- - **Runtime Registration**: Additional directories require explicit registration via `register_directory` tool
939
+ - **Runtime Registration**: Additional directories require registration via `register_directory`, which asks the user to confirm by default (`--runtime-registration`)
940
+ - **Resource Limits**: Full-file text reads are capped at 10 MB, `attach_image` at 10 MB per image and 20 MB per call, and Office/ODF documents are checked for zip bombs before parsing
912
941
  - **Atomic Validation**: Paths validated before any file operations begin
913
942
  - **Cross-Platform Safety**: Proper handling of Windows/Unix path differences and UNC paths
914
943
 
@@ -917,7 +946,7 @@ This MCP server implements enterprise-grade security controls to protect against
917
946
  1. **Minimize Approved Directories**: Only approve directories that require AI access
918
947
  2. **Use Directory Filtering**: Exclude sensitive folders (e.g., `.git`, `node_modules`) from listings
919
948
  3. **Limit Tool Access**: Enable only necessary tools via `--enabled-tools` or `--enabled-tool-categories`
920
- 4. **Command Approval**: Pre-approve safe commands via `--approved-commands`; require approval for others
949
+ 4. **Command Approval**: Approve only the commands you need via `--approved-commands`; avoid shells and interpreters (`bash`, `sh`, `powershell`, `node`, `python`, `eval`), which can run arbitrary code
921
950
  5. **Monitor Operations**: Review MCP client logs for unexpected access attempts
922
951
  6. **Regular Updates**: Keep the server updated to receive security patches
923
952
 
@@ -928,13 +957,19 @@ This server has been comprehensively audited against known vulnerabilities and s
928
957
  **CVE Protection Status:**
929
958
 
930
959
  - ✅ CVE-2025-54794 (Path Restriction Bypass) - **FIXED**
931
- - ✅ CVE-2025-54795 (Command Injection) - **PROTECTED**
932
- - ✅ CVE-2025-53109 (Symlink Attacks) - **PROTECTED**
960
+ - ✅ CVE-2025-54795 (Command Injection) - **PROTECTED** (escaped-quote and newline variants closed in 1.3.0)
961
+ - ✅ CVE-2025-53109 (Symlink Attacks) - **PROTECTED** (`make_directory` and `execute_shell` gaps closed in 1.3.0)
933
962
  - ✅ CVE-2025-53110 (Directory Containment Bypass) - **PROTECTED**
934
- - ✅ Shell Execution Directory Bypass - **FIXED** (November 2024)
963
+ - ✅ Shell Execution Directory Bypass - **FIXED** in 1.3.0. The November 2024 fix was incomplete: slash-prefixed paths, attached redirections and symlinked paths could still escape
935
964
 
936
965
  **Latest Security Audits:**
937
966
 
967
+ - 📋 Security hardening release 1.3.0 - October 2026
968
+ - **Scope**: Independent audit of GitHub issue #3 plus a Snyk Open Source / Snyk Code / container scan and manual review
969
+ - **Fixed**: newline command chaining past the allowlist, shell path validation gaps (slash paths, redirections, symlinks), self-approved dangerous commands, `make_directory` symlink escape, AI-initiated directory registration without consent, `.env` loading from the working directory, PDF image handling that could crash the server or fetch remote URLs, unbounded regex/read/decompression resource use, symlink dereferencing in directory copies
970
+ - **Dependencies**: refreshed with a 14-day release quarantine; all packages pass `npm audit signatures`
971
+ - See [CHANGELOG.md](CHANGELOG.md) for details
972
+
938
973
  - 📋 [Snyk Vulnerability Audit Report - November 2025](docs/SNYK_VULNERABILITY_AUDIT_2025.md)
939
974
  - **Status**: 5/6 Snyk findings validated as false positives, 1 finding fixed
940
975
  - **Risk Level**: LOW - Comprehensive path traversal protection verified
@@ -945,10 +980,10 @@ This server has been comprehensively audited against known vulnerabilities and s
945
980
  - **Focus**: CVE-2025-54794/54795 pattern analysis and mitigation strategies
946
981
  - **Date**: November 4, 2025 (Manual CVE Research)
947
982
  - 📋 [Shell Command Directory Bypass Audit - November 2025](docs/SHELL_COMMAND_AUDIT_2025-11-04.md)
948
- - **Status**: ✅ Fixed November 2024 (Retrospective documentation)
983
+ - **Status**: Partially fixed November 2024; completed in 1.3.0 (October 2026)
949
984
  - **Issue**: Shell commands previously could access files outside approved directories via absolute paths
950
985
  - **Severity**: HIGH (CVSS ~7.5) - Path traversal via command arguments
951
- - **Fix Status**: ✅ FIXED - Path extraction and validation implemented
986
+ - **Fix Status**: ✅ FIXED in 1.3.0 - symlink-aware validation of all operands, including redirections
952
987
  - **Test Coverage**: 419 lines of comprehensive tests, all passing
953
988
  - 📋 [Security Test Coverage Summary](docs/SECURITY_TEST_SUMMARY.md)
954
989
  - **Test Suite**: 2000+ lines of security-focused tests in `src/tests/`
@@ -999,7 +1034,7 @@ This server has been comprehensively audited against known vulnerabilities and s
999
1034
  **Attach Image Tool** (`attach_image`):
1000
1035
 
1001
1036
  - Attaches images for AI vision analysis (requires vision-capable MCP client)
1002
- - **Supported formats**: PNG, JPEG, GIF, WebP, BMP, SVG
1037
+ - **Supported formats**: PNG, JPEG, GIF, WebP (SVG is returned as markup text; BMP is not supported)
1003
1038
  - **Batch support**: Can attach single image or multiple images in one call
1004
1039
  - Images are presented to the AI as if uploaded directly by the user
1005
1040
  - Enables visual analysis: reading text in images, analyzing diagrams, describing scenes
package/dist/cli.js CHANGED
@@ -18,6 +18,22 @@ if (isMCP) {
18
18
  // as the MCP SDK uses those for JSON-RPC protocol communication
19
19
  }
20
20
  import { runServer } from "./server/index.js";
21
+ // Defense in depth (VFO-13): a promise rejection that a third-party library
22
+ // fails to handle must not terminate the stdio server (Node's default for
23
+ // unhandled rejections is to crash). Log one line to STDERR - console.* is
24
+ // a no-op in MCP mode and stdout carries the JSON-RPC stream - and keep
25
+ // running. uncaughtException keeps Node's default (fatal) behavior.
26
+ process.on("unhandledRejection", (reason) => {
27
+ try {
28
+ const detail = reason instanceof Error
29
+ ? `${reason.name}: ${reason.message}`
30
+ : String(reason);
31
+ process.stderr.write(`[vulcan-file-ops] unhandledRejection (ignored): ${detail.replace(/\s+/g, " ").slice(0, 2000)}\n`);
32
+ }
33
+ catch {
34
+ // Never let diagnostics throw.
35
+ }
36
+ });
21
37
  // Run the server and handle any fatal errors
22
38
  runServer().catch((error) => {
23
39
  // Only show errors when not running under MCP (to avoid protocol corruption)
@@ -16,7 +16,7 @@ if (_isMCP) {
16
16
  }
17
17
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
18
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
- import { CallToolRequestSchema, ListToolsRequestSchema, PingRequestSchema, RootsListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
19
+ import { CallToolRequestSchema, ListToolsRequestSchema, PingRequestSchema, RootsListChangedNotificationSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
20
20
  import fs from "fs/promises";
21
21
  import { readFileSync } from "fs";
22
22
  import path from "path";
@@ -34,16 +34,19 @@ const VERSION = packageJson.version;
34
34
  // Import tool handlers
35
35
  import { getReadTools } from "../tools/read-tools.js";
36
36
  import { getWriteTools } from "../tools/write-tools.js";
37
- import { getFileSystemTools } from "../tools/filesystem-tools.js";
37
+ import { getFileSystemTools, isRuntimeRegistrationPolicy, setRuntimeRegistrationPolicy, setDirectoryRegistrationConsentHandler, RUNTIME_REGISTRATION_POLICIES, } from "../tools/filesystem-tools.js";
38
38
  import { getSearchTools } from "../tools/search-tools.js";
39
39
  import { initializeShellTool, getShellTools } from "../tools/shell-tool.js";
40
40
  // Configuration storage
41
41
  let allowedDirectories = [];
42
42
  let approvedFoldersFromArgs = [];
43
43
  let approvedCommandsFromArgs = [];
44
+ let dangerousCommandsFromArgs = [];
44
45
  let ignoredFolders = [];
45
46
  let enabledToolCategories = [];
46
47
  let enabledTools = [];
48
+ let runtimeRegistrationPolicy = "confirm";
49
+ let envFileFromArgs = null;
47
50
  // Command line argument parsing
48
51
  function parseArguments() {
49
52
  const args = process.argv.slice(2);
@@ -65,6 +68,12 @@ function parseArguments() {
65
68
  console.error(" --enabled-tool-categories <cats...> Enable specific tool categories (comma-separated)");
66
69
  console.error(" --enabled-tools <tools...> Enable specific tools (comma-separated)");
67
70
  console.error(" --approved-commands <cmds...> Allow specific shell commands (comma-separated)");
71
+ console.error(" --allow-dangerous-commands <cmds...> Approved commands that may run even when they match");
72
+ console.error(" a dangerous pattern (e.g. rm -rf); comma-separated");
73
+ console.error(" --commands-env-file <path> Read APPROVED_COMMANDS from this .env-format file (only");
74
+ console.error(" that key is used; nothing is added to the environment)");
75
+ console.error(" --runtime-registration <mode> register_directory policy: confirm (default; user must");
76
+ console.error(" approve via the MCP client), allow (no prompt), deny");
68
77
  console.error(" --help, -h Show this help message");
69
78
  console.error(" --version, -v Show version information");
70
79
  console.error("");
@@ -157,6 +166,59 @@ function parseArguments() {
157
166
  .filter((tool) => tool.length > 0));
158
167
  continue;
159
168
  }
169
+ if (arg === "--runtime-registration" ||
170
+ arg.startsWith("--runtime-registration=")) {
171
+ let value;
172
+ if (arg.includes("=")) {
173
+ value = arg.slice(arg.indexOf("=") + 1);
174
+ }
175
+ else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
176
+ value = args[++i];
177
+ }
178
+ if (!value || !isRuntimeRegistrationPolicy(value.trim())) {
179
+ console.error(`Error: Invalid value for --runtime-registration: '${value ?? ""}'. ` +
180
+ `Expected one of: ${RUNTIME_REGISTRATION_POLICIES.join(", ")}`);
181
+ console.error("Run with --help for usage information.");
182
+ process.exit(1);
183
+ }
184
+ runtimeRegistrationPolicy = value.trim();
185
+ parsingIgnoredFolders = false;
186
+ parsingApprovedFolders = false;
187
+ parsingEnabledToolCategories = false;
188
+ parsingEnabledTools = false;
189
+ parsingApprovedCommands = false;
190
+ continue;
191
+ }
192
+ // Node.js itself scans ALL argv entries (even after the script) for
193
+ // --env-file and applies NODE_OPTIONS from that file to this process,
194
+ // so the server must not use that name.
195
+ if (arg === "--env-file" || arg.startsWith("--env-file=")) {
196
+ console.error("Error: --env-file is reserved by Node.js. Use --commands-env-file <path> instead.");
197
+ console.error("Run with --help for usage information.");
198
+ process.exit(1);
199
+ }
200
+ if (arg === "--commands-env-file" ||
201
+ arg.startsWith("--commands-env-file=")) {
202
+ let value;
203
+ if (arg.includes("=")) {
204
+ value = arg.slice(arg.indexOf("=") + 1);
205
+ }
206
+ else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
207
+ value = args[++i];
208
+ }
209
+ if (!value || value.trim().length === 0) {
210
+ console.error("Error: --commands-env-file requires a file path");
211
+ console.error("Run with --help for usage information.");
212
+ process.exit(1);
213
+ }
214
+ envFileFromArgs = value.trim();
215
+ parsingIgnoredFolders = false;
216
+ parsingApprovedFolders = false;
217
+ parsingEnabledToolCategories = false;
218
+ parsingEnabledTools = false;
219
+ parsingApprovedCommands = false;
220
+ continue;
221
+ }
160
222
  if (arg === "--approved-commands") {
161
223
  parsingApprovedCommands = true;
162
224
  parsingIgnoredFolders = false;
@@ -176,6 +238,23 @@ function parseArguments() {
176
238
  .filter((cmd) => cmd.length > 0));
177
239
  continue;
178
240
  }
241
+ if (arg === "--allow-dangerous-commands") {
242
+ parsingApprovedCommands = false;
243
+ parsingIgnoredFolders = false;
244
+ parsingApprovedFolders = false;
245
+ parsingEnabledToolCategories = false;
246
+ parsingEnabledTools = false;
247
+ const commands = [];
248
+ while (i + 1 < args.length && !args[i + 1].startsWith("--")) {
249
+ commands.push(args[i + 1]);
250
+ i++;
251
+ }
252
+ dangerousCommandsFromArgs = commands.flatMap((c) => c
253
+ .split(",")
254
+ .map((cmd) => cmd.trim())
255
+ .filter((cmd) => cmd.length > 0));
256
+ continue;
257
+ }
179
258
  // If we're not parsing a flag value, this might be an unrecognized argument
180
259
  if (!parsingIgnoredFolders &&
181
260
  !parsingApprovedFolders &&
@@ -328,6 +407,7 @@ async function initializeDirectories() {
328
407
  setIgnoredFolders(ignoredFolders);
329
408
  // Set individual enabled tools (categories are combined dynamically in tool handlers)
330
409
  setEnabledTools(enabledTools);
410
+ setRuntimeRegistrationPolicy(runtimeRegistrationPolicy);
331
411
  // Load shell command configuration
332
412
  let finalApprovedCommands = [];
333
413
  // Priority 1: --approved-commands from CLI (supersedes .env)
@@ -337,29 +417,34 @@ async function initializeDirectories() {
337
417
  console.error(` Approved commands (from CLI): ${finalApprovedCommands.join(", ")}`);
338
418
  }
339
419
  }
340
- else {
341
- // Priority 2: Load from .env file
420
+ else if (envFileFromArgs) {
421
+ // Priority 2: explicit --commands-env-file. Security (VFO-08): never load a .env
422
+ // implicitly from the working directory, and never inject its variables
423
+ // into process.env (shell commands inherit it). Only APPROVED_COMMANDS
424
+ // is read.
425
+ const envPath = path.resolve(process.cwd(), expandHome(envFileFromArgs));
342
426
  try {
343
- const envPath = path.join(process.cwd(), ".env");
344
- dotenv.config({ path: envPath, quiet: true });
345
- if (process.env.APPROVED_COMMANDS) {
346
- finalApprovedCommands = process.env.APPROVED_COMMANDS.split(",")
347
- .map((c) => c.trim())
348
- .filter((c) => c.length > 0);
349
- if (!isMCP) {
350
- console.error(` Approved commands (from .env): ${finalApprovedCommands.join(", ")}`);
351
- }
427
+ const parsedEnv = dotenv.parse(readFileSync(envPath));
428
+ finalApprovedCommands = (parsedEnv.APPROVED_COMMANDS ?? "")
429
+ .split(",")
430
+ .map((c) => c.trim())
431
+ .filter((c) => c.length > 0);
432
+ if (!isMCP) {
433
+ console.error(` Approved commands (from ${envPath}): ${finalApprovedCommands.join(", ")}`);
352
434
  }
353
435
  }
354
436
  catch (error) {
437
+ const message = error instanceof Error ? error.message : String(error);
438
+ console.error(`Error: Could not read --commands-env-file ${envPath}: ${message}`);
439
+ // In MCP mode, don't exit - continue with no approved commands
355
440
  if (!isMCP) {
356
- console.error(" Note: Could not load .env file (this is okay if using CLI args)");
441
+ process.exit(1);
357
442
  }
358
443
  }
359
444
  }
360
445
  // Initialize shell tool with approved commands
361
446
  if (finalApprovedCommands.length > 0) {
362
- initializeShellTool(finalApprovedCommands);
447
+ initializeShellTool(finalApprovedCommands, dangerousCommandsFromArgs);
363
448
  if (!isMCP) {
364
449
  console.error(`Initialized shell tool with ${finalApprovedCommands.length} approved command(s)`);
365
450
  }
@@ -404,6 +489,42 @@ const server = new Server({
404
489
  // _clientCapabilities and _clientVersion never get set, and protocol version
405
490
  // negotiation is bypassed (should respond with max supported ≤ client's requested).
406
491
  // Instructions are injected at runtime in runServer() after directories initialize.
492
+ // Security (VFO-07): under the default "confirm" policy, register_directory
493
+ // asks the human through MCP elicitation before widening the sandbox.
494
+ // Clients without elicitation support get "unsupported" (registration refused).
495
+ const REGISTRATION_CONSENT_TIMEOUT_MS = 5 * 60 * 1000;
496
+ setDirectoryRegistrationConsentHandler(async (realPath) => {
497
+ if (!server.getClientCapabilities()?.elicitation) {
498
+ return "unsupported";
499
+ }
500
+ try {
501
+ const result = await server.elicitInput({
502
+ message: `Allow the AI assistant to read and modify files in:\n${realPath}\n\n` +
503
+ "Only approve directories you trust it to change.",
504
+ requestedSchema: {
505
+ type: "object",
506
+ properties: {
507
+ allow: {
508
+ type: "boolean",
509
+ title: "Allow access",
510
+ description: "Grant read/write access to this directory for this session",
511
+ },
512
+ },
513
+ required: ["allow"],
514
+ },
515
+ }, { timeout: REGISTRATION_CONSENT_TIMEOUT_MS });
516
+ return result.action === "accept" && result.content?.allow === true
517
+ ? "accepted"
518
+ : "declined";
519
+ }
520
+ catch (error) {
521
+ if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {
522
+ return "unsupported";
523
+ }
524
+ // Timeouts, invalid responses, transport errors: fail closed
525
+ return "declined";
526
+ }
527
+ });
407
528
  // Ping handler - for health checks
408
529
  server.setRequestHandler(PingRequestSchema, async () => {
409
530
  return {};