Skip to content

Commit 1947617

Browse files
committed
feat(security): Add glob patterns and expand sensitive-file coverage
Added glob pattern support (`*`, `?`) in `mcp-server-security-sensitive-file-patterns` via `wildcard-to-regexp`. Patterns like `~/.authinfo*` now correctly match all variants (`~/.authinfo`, `~/.authinfo.gpg`, `~/.authinfo.enc`, etc.). Extended sensitive file protection to cover `copy-file`, `rename-file`, `write-region`, `append-to-file`, `write-file`, and `insert-file-contents-literally`, blocking both reads from and writes to sensitive paths even when the function itself is allowed. Added new dangerous functions: `append-to-file`, `async-shell-command`, `directory-files`, `directory-files-recursively`, `insert-file-contents-literally`, `make-network-process`, `make-process`, `open-network-stream`, `setenv`, `with-temp-file`, `write-file`. Removed non-functional entries: `process-environment` (variable), `shell-environment` (non-standard), `require` (too restrictive), `save-current-buffer`, `set-buffer`, `switch-to-buffer` (low-level primitives). Fixed `mcp-server-security--is-sensitive-file`: patterns using `~/` prefix are now expanded with `expand-file-name` before comparison (issue #9). Fixed `mcp-server-security--is-dangerous-operation`: calling `symbol-name` on string operation IDs no longer raises `wrong-type-argument: symbolp`. Added comprehensive unit test suite (79 ERT tests) for `mcp-server-security`. Documented known limitation: static form walker does not recurse into `let`-binding positions and cannot detect dynamically-constructed function names (issue #10).
1 parent da21de8 commit 1947617

File tree

6 files changed

+706
-43
lines changed

6 files changed

+706
-43
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.0] - 2026-03-31
9+
10+
### Fixed
11+
- `mcp-server-security--is-sensitive-file`: patterns using `~/` prefix (e.g. `"~/.ssh/"`) were never matched because the input path was expanded with `expand-file-name` but the patterns were not, causing the literal `~` to fail against an absolute path (issue #9)
12+
- `mcp-server-security--is-dangerous-operation`: calling `symbol-name` on a string operation ID (e.g. `"access-sensitive-file:find-file"`) raised `wrong-type-argument: symbolp` instead of returning a result
13+
- Sensitive file check was only applied to four functions (`find-file`, `find-file-noselect`, `view-file`, `insert-file-contents`); functions like `copy-file` and `rename-file` could still access sensitive files when placed in `mcp-server-security-allowed-dangerous-functions`
14+
15+
### Added
16+
- Glob pattern support (`*`, `?`) in `mcp-server-security-sensitive-file-patterns`: patterns such as `"~/.authinfo*"` now correctly match all variants (`~/.authinfo.gpg`, `~/.authinfo.enc`, etc.) via `wildcard-to-regexp`
17+
- Sensitive file check now covers `copy-file` (args 1 and 2), `rename-file` (args 1 and 2), `write-region` (arg 3), `append-to-file` (arg 3), `write-file` (arg 1), and `insert-file-contents-literally` (arg 1), blocking both reads from and writes to sensitive paths
18+
- New entries in default `mcp-server-security-dangerous-functions`: `append-to-file`, `async-shell-command`, `directory-files`, `directory-files-recursively`, `insert-file-contents-literally`, `make-network-process`, `make-process`, `open-network-stream`, `setenv`, `with-temp-file`, `write-file`
19+
- Removed entries from default `mcp-server-security-dangerous-functions` that were either non-functional or overly broad: `process-environment` (a variable, not a function; the form walker never matched it), `shell-environment` (not a standard Emacs function), `require` (too restrictive for legitimate use), `save-current-buffer` and `set-buffer` (low-level context-switching primitives with no inherent danger), `switch-to-buffer` (interactive UI command with no security impact)
20+
- Comprehensive unit test suite for `mcp-server-security` (79 ERT tests total)
21+
- Known limitation documented and tracked: static form walker does not recurse into `let`-binding value positions and cannot detect dynamically-constructed function names (`funcall` + `intern`); see issue #10
22+
823
## [0.4.0] - 2026-01-08
924

1025
### Added
@@ -68,6 +83,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6883
- Comprehensive test suite
6984
- Demo images for theme change and poem writing
7085

86+
[0.6.0]: https://github.com/rhblind/emacs-mcp-server/compare/v0.5.0...v0.6.0
7187
[0.4.0]: https://github.com/rhblind/emacs-mcp-server/compare/v0.3.0...v0.4.0
7288
[0.3.0]: https://github.com/rhblind/emacs-mcp-server/compare/v0.2.0...v0.3.0
7389
[0.2.0]: https://github.com/rhblind/emacs-mcp-server/compare/v0.1.0...v0.2.0

README.md

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ Retrieves errors and warnings from flycheck or flymake (auto-detected per buffer
177177
### Security Features
178178

179179
**Permission System:**
180-
- **Dangerous function protection** - Functions like `delete-file`, `shell-command`, `find-file` require user approval
181-
- **Sensitive file protection** - Automatic blocking of credential files (`.authinfo`, `.netrc`, SSH keys, etc.)
180+
- **Dangerous function protection** - Functions like `delete-file`, `shell-command`, `find-file`, `write-file`, `make-network-process`, `make-process`, `async-shell-command`, `setenv` require user approval
181+
- **Sensitive file protection** - Automatic blocking of credential files (`.authinfo`, `.netrc`, SSH keys, etc.) for both reads and writes, even when the function itself is allowed
182182
- **Buffer access controls** - Protection for sensitive buffers like `*Messages*`, `*shell*`, `*terminal*`
183183
- **Permission caching** - Decisions are remembered for the session to avoid repeated prompts
184184
- **Audit logging** - Complete trail of all security events and decisions
@@ -198,9 +198,9 @@ Retrieves errors and warnings from flycheck or flymake (auto-detected per buffer
198198

199199
The security model provides defense-in-depth but has known limitations:
200200

201-
- **Blocklist bypass via indirection** - The dangerous function blocklist checks direct function calls, but `funcall`, `apply`, or `(intern "shell-command")` can bypass it
202-
- **Macro evaluation** - Macros that expand to dangerous code execute at compile-time before security checks
203-
- **Dynamic function construction** - Code can construct function names at runtime to evade static analysis
201+
- **`let`-binding positions not fully checked** - The static form walker does not recurse into `let`/`let*` binding-value positions, so `(let ((x (shell-command "id"))) x)` bypasses the blocklist. Tracked in [#10](https://github.com/rhblind/emacs-mcp-server/issues/10).
202+
- **Dynamic function construction** - Code can construct function names at runtime to evade static analysis: `(funcall (intern "delete-file") "/tmp/test")` passes the checker. Tracked in [#10](https://github.com/rhblind/emacs-mcp-server/issues/10).
203+
- **Macro expansion** - The form walker operates on unexpanded forms. Macros that wrap dangerous calls in non-tail positions (other than `let` bindings) may not be caught.
204204

205205
**This means:** Using `eval-elisp` requires trusting the LLM completely. The security controls reduce risk of accidental harm but cannot prevent a determined adversary.
206206

@@ -211,13 +211,15 @@ For higher security requirements, consider:
211211

212212
### Default Protected Files
213213

214-
The server automatically protects these sensitive file patterns:
214+
The server automatically protects these sensitive file patterns.
215+
216+
Patterns starting with `~/` or `/` are matched as path prefixes after expanding `~`. Patterns containing `*` or `?` are treated as shell globs (e.g. `~/.authinfo*` matches `~/.authinfo`, `~/.authinfo.gpg`, `~/.authinfo.enc`, etc.). Bare filename patterns (no leading `~/`) match against the file's basename.
215217

216218
**Authentication Files:**
217-
- `~/.authinfo*` - Emacs authentication data
218-
- `~/.netrc*` - Network authentication credentials
219-
- `~/.ssh/` - SSH keys and configuration
220-
- `~/.gnupg/` - GPG keys and configuration
219+
- `~/.authinfo` and variants - Emacs authentication data
220+
- `~/.netrc` and variants - Network authentication credentials
221+
- `~/.ssh/` - SSH keys and configuration (entire directory)
222+
- `~/.gnupg/` - GPG keys and configuration (entire directory)
221223

222224
**Cloud & Service Credentials:**
223225
- `~/.aws/` - AWS credentials
@@ -232,17 +234,25 @@ The server automatically protects these sensitive file patterns:
232234

233235
### Security Configuration
234236

237+
The default settings aim for a reasonable balance between security and usability: the most commonly dangerous functions are blocked, well-known credential files are protected, and operations fail silently rather than prompting. These defaults will not suit every workflow. You are encouraged to review them and adjust to your own needs, for example by tightening the dangerous function list, adding project-specific sensitive file patterns, or enabling prompts instead of silent denial.
238+
235239
Users have complete control over the security model through customizable settings:
236240

237241
**Customize Sensitive Files:**
238242
```elisp
239-
;; Add your own sensitive file patterns
243+
;; Add your own sensitive file patterns.
244+
;; Patterns starting with ~/ or / are matched as path prefixes (after expanding ~).
245+
;; Patterns containing * or ? are treated as shell globs.
246+
;; Bare filename patterns match against the file's basename.
240247
(setq mcp-server-security-sensitive-file-patterns
241-
'("~/.authinfo" "~/.ssh/" "~/my-secrets/" "*.key"))
248+
'("~/.authinfo*" ; glob: matches .authinfo, .authinfo.gpg, .authinfo.enc, ...
249+
"~/.ssh/" ; prefix: matches everything under ~/.ssh/
250+
"~/my-secrets/" ; prefix: matches everything under ~/my-secrets/
251+
".key")) ; basename: matches any file whose name contains ".key"
242252
243253
;; Allow specific sensitive files without prompting
244254
(setq mcp-server-security-allowed-sensitive-files
245-
'("~/.authinfo" "~/safe-config.txt"))
255+
'("~/.ssh/known_hosts" "~/safe-config.txt"))
246256
```
247257

248258
**Customize Dangerous Functions:**
@@ -258,7 +268,7 @@ Users have complete control over the security model through customizable setting
258268

259269
**Global Security Settings:**
260270
```elisp
261-
;; Disable all permission prompts (not recommended)
271+
;; Block dangerous operations silently without prompting (default)
262272
(setq mcp-server-security-prompt-for-permissions nil)
263273
264274
;; Customize execution timeout
@@ -289,7 +299,7 @@ M-x customize-group RET mcp-server RET
289299
```elisp
290300
;; More relaxed for trusted development scenarios
291301
(setq mcp-server-security-allowed-dangerous-functions
292-
'(find-file dired switch-to-buffer insert-file-contents))
302+
'(find-file dired insert-file-contents))
293303
;; Keep file protections for credentials
294304
(setq mcp-server-security-sensitive-file-patterns
295305
'("~/.authinfo*" "~/.netrc*" "~/.ssh/" "~/.gnupg/"))
@@ -327,7 +337,7 @@ M-x mcp-server-security-clear-permissions ; Reset all cached permissions
327337
4. **Monitor credentials** - Be especially careful with any files containing passwords or API keys
328338
5. **Test security** - Verify your configuration blocks unauthorized access as expected
329339

330-
Operations requiring explicit permission include file system operations (`delete-file`, `write-region`), process execution (`shell-command`, `call-process`), system functions (`kill-emacs`, `server-start`), and access to sensitive files or buffers.
340+
Operations requiring explicit permission include file system operations (`delete-file`, `write-region`, `write-file`, `append-to-file`, `copy-file`, `rename-file`, `with-temp-file`), process execution (`shell-command`, `call-process`, `start-process`), network access (`make-network-process`, `open-network-stream`, `url-retrieve`), directory enumeration (`directory-files`, `directory-files-recursively`), system functions (`kill-emacs`, `server-start`), and access to sensitive files or buffers.
331341

332342
## Management Commands
333343

mcp-server-security.el

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,41 +22,46 @@
2222
;;; Variables
2323

2424
(defcustom mcp-server-security-dangerous-functions
25-
'(browse-url
25+
'(append-to-file
26+
async-shell-command
27+
browse-url
2628
call-process
2729
copy-file
2830
delete-directory
2931
delete-file
32+
directory-files
33+
directory-files-recursively
3034
dired
3135
eval
3236
find-file
3337
find-file-literally
3438
find-file-noselect
3539
getenv
3640
insert-file-contents
41+
insert-file-contents-literally
3742
kill-emacs
3843
load
3944
make-directory
40-
process-environment
45+
make-network-process
46+
make-process
47+
open-network-stream
4148
rename-file
42-
require
4349
save-buffers-kill-emacs
4450
save-buffers-kill-terminal
45-
save-current-buffer
4651
server-force-delete
4752
server-start
48-
set-buffer
4953
set-file-modes
5054
set-file-times
55+
setenv
5156
shell-command
5257
shell-command-to-string
53-
shell-environment
5458
start-process
55-
switch-to-buffer
5659
url-retrieve
5760
url-retrieve-synchronously
5861
view-file
5962
with-current-buffer
63+
with-temp-file
64+
write-file
6065
write-region)
6166
"List of functions that require permission before execution.
6267
Users can customize this list to add or remove functions that should
@@ -171,12 +176,19 @@ Returns 'yes, 'no, or 'always."
171176
(?! 'always))))
172177

173178
(defun mcp-server-security--is-dangerous-operation (operation)
174-
"Check if OPERATION is considered dangerous."
175-
;; First check if function is explicitly allowed
176-
(unless (member operation mcp-server-security-allowed-dangerous-functions)
177-
;; Then check if it's in the dangerous functions list or matches dangerous patterns
178-
(or (member operation mcp-server-security-dangerous-functions)
179-
(string-match-p "delete\\|kill\\|remove\\|destroy" (symbol-name operation)))))
179+
"Check if OPERATION is considered dangerous.
180+
OPERATION is either a symbol (a function name) or a string synthetic ID
181+
such as \"access-sensitive-file:find-file\" produced by the sensitive
182+
file/buffer check paths. String ops prefixed with \"access-sensitive-\"
183+
are always treated as dangerous so they are denied when prompting is off."
184+
(if (stringp operation)
185+
;; Synthetic string IDs from sensitive file/buffer checks are dangerous
186+
(string-prefix-p "access-sensitive-" operation)
187+
;; Symbol: check the allowed list, dangerous list, and name patterns
188+
(unless (member operation mcp-server-security-allowed-dangerous-functions)
189+
(or (member operation mcp-server-security-dangerous-functions)
190+
(string-match-p "delete\\|kill\\|remove\\|destroy"
191+
(symbol-name operation))))))
180192

181193
(defun mcp-server-security--is-sensitive-file (path)
182194
"Check if PATH points to a sensitive file."
@@ -186,10 +198,25 @@ Returns 'yes, 'no, or 'always."
186198
(unless (cl-some (lambda (allowed-file)
187199
(string-equal (expand-file-name allowed-file) expanded-path))
188200
mcp-server-security-allowed-sensitive-files)
189-
;; Then check if it matches any sensitive patterns
201+
;; Then check if it matches any sensitive patterns.
202+
;;
203+
;; Patterns starting with ~ or / are expanded before comparison so that
204+
;; e.g. "~/.ssh/" correctly matches the expanded path "/home/user/.ssh/id_rsa".
205+
;;
206+
;; Patterns containing * or ? are treated as shell glob wildcards via
207+
;; `wildcard-to-regexp', so "~/.authinfo*" matches "~/.authinfo.gpg" etc.
208+
;;
209+
;; Bare filename patterns (no leading ~ or /) are matched literally
210+
;; against the file's basename only.
190211
(cl-some (lambda (pattern)
191-
(or (string-match-p (regexp-quote pattern) expanded-path)
192-
(string-match-p pattern (file-name-nondirectory expanded-path))))
212+
(if (string-match-p "^[~/]" pattern)
213+
(let ((expanded-pattern (expand-file-name pattern)))
214+
(if (string-match-p "[*?]" pattern)
215+
(string-match-p (wildcard-to-regexp expanded-pattern)
216+
expanded-path)
217+
(string-prefix-p expanded-pattern expanded-path)))
218+
(string-match-p (regexp-quote pattern)
219+
(file-name-nondirectory expanded-path))))
193220
mcp-server-security-sensitive-file-patterns)))))
194221

195222
(defun mcp-server-security--is-sensitive-buffer (buffer-name)
@@ -279,14 +306,32 @@ to allow, or set `mcp-server-security-prompt-for-permissions' to t to prompt" fo
279306
(error "Security: `%s' is blocked. Add it to `mcp-server-security-allowed-dangerous-functions' \
280307
to allow, or set `mcp-server-security-prompt-for-permissions' to t to prompt" func)))
281308

282-
;; Special checks for file access functions
283-
(when (memq func '(find-file find-file-noselect view-file insert-file-contents))
284-
(let ((file-path (car args)))
285-
(when (and file-path (stringp file-path))
286-
(when (mcp-server-security--is-sensitive-file file-path)
287-
(unless (mcp-server-security-check-permission
288-
(format "access-sensitive-file:%s" func) file-path)
289-
(error "Permission denied for sensitive file access: %s" file-path))))))
309+
;; Special checks for file access functions.
310+
;; Each entry maps a function to the 1-based positions of its file-path
311+
;; arguments. Both read and write paths are covered so that sensitive
312+
;; files cannot be read from OR written to, even when the function is
313+
;; listed in `mcp-server-security-allowed-dangerous-functions'.
314+
(let ((file-arg-specs
315+
'((find-file . (1))
316+
(find-file-noselect . (1))
317+
(find-file-literally . (1))
318+
(view-file . (1))
319+
(insert-file-contents . (1))
320+
(insert-file-contents-literally . (1))
321+
(write-file . (1))
322+
(copy-file . (1 2))
323+
(rename-file . (1 2))
324+
(write-region . (3))
325+
(append-to-file . (3)))))
326+
(when-let ((positions (alist-get func file-arg-specs)))
327+
(dolist (pos positions)
328+
(let ((file-path (nth (1- pos) args)))
329+
(when (and file-path (stringp file-path))
330+
(when (mcp-server-security--is-sensitive-file file-path)
331+
(unless (mcp-server-security-check-permission
332+
(format "access-sensitive-file:%s" func) file-path)
333+
(error "Permission denied for sensitive file access: %s"
334+
file-path))))))))
290335

291336
;; Special checks for buffer access functions
292337
(when (memq func '(switch-to-buffer set-buffer with-current-buffer))

mcp-server.el

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
;; Author: Claude Code + Rolf Håvard Blindheim<rhblind@gmail.com>
66
;; URL: https://github.com/rhblind/emacs-mcp-server
77
;; Keywords: mcp, protocol, integration, tools
8-
;; Version: 0.5.0
8+
;; Version: 0.6.0
99
;; Package-Requires: ((emacs "27.1"))
1010

1111
;; This file is NOT part of GNU Emacs.
@@ -61,7 +61,7 @@
6161

6262
;;; Constants
6363

64-
(defconst mcp-server-version "0.5.0"
64+
(defconst mcp-server-version "0.6.0"
6565
"Version of the Emacs MCP server.")
6666

6767
(defconst mcp-server-protocol-version "2024-11-05"

0 commit comments

Comments
 (0)