You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The static Elisp form checker in mcp-server-security--check-form-safety has two structural limitations that allow dangerous operations to bypass the blocklist. These require architectural changes and are tracked here separately from the immediate fixes in #9.
Bypass 1: let-binding positions are not recursed into
The walker iterates the args of a form, but treats the car of a list argument (the "function" position) as opaque when it is not a symbol. In a let binding list ((x (shell-command "id"))), the structure seen by the walker is:
Because func is not a symbol, the dangerous-function check is skipped. Because args is nil, no recursion happens. The inner call (shell-command "id") is never visited.
Verified bypass:
(let ((x (shell-command"id"))) x) ; passes the checker, executes shell-command
The same pattern applies to let*, cl-loop, dotimes, and any macro form that wraps dangerous calls in a binding position rather than a tail position.
Bypass 2: Dynamic function-name construction via funcall/apply + intern
The checker operates on static symbols. A function name constructed at runtime is invisible to it:
Any variant that avoids placing the dangerous symbol in a statically-visible position evades the check.
Impact
Both bypasses require the attacker to have already gained eval-elisp tool access (which is itself gated by destructiveHint and the function-level blocklist). They are therefore a second layer of concern, but relevant when users place trust in the Emacs-side enforcement.
Proposed approaches
For bypass 1
Run macroexpand-all on the form before walking it. This converts let, let*, when, unless, dolist, etc. into their primitive equivalents, making all subforms visible to the walker. Caveats: adds latency for complex forms; user-defined macros may not expand predictably.
For bypass 2
The static checker cannot close this gap by design — it cannot evaluate arbitrary code to determine what (intern ...) will return. Options:
Add funcall, apply, and intern to mcp-server-security-dangerous-functions by default, so they are blocked unless explicitly allowed. This is a breaking change for legitimate use.
Document that the function-level blocklist is the primary enforcement layer and the form walker is a best-effort defence-in-depth measure, not a complete sandbox.
Related
Fixed in the same PR: issue Confusion about access control settings #9 (sensitive file pattern matching), missing dangerous functions (with-temp-file, write-file, append-to-file, make-network-process, open-network-stream, directory-files, insert-file-contents-literally), glob pattern support in mcp-server-security-sensitive-file-patterns.
Overview
The static Elisp form checker in
mcp-server-security--check-form-safetyhas two structural limitations that allow dangerous operations to bypass the blocklist. These require architectural changes and are tracked here separately from the immediate fixes in #9.Bypass 1:
let-binding positions are not recursed intoThe walker iterates the
argsof a form, but treats thecarof a list argument (the "function" position) as opaque when it is not a symbol. In aletbinding list((x (shell-command "id"))), the structure seen by the walker is:Because
funcis not a symbol, the dangerous-function check is skipped. Becauseargsisnil, no recursion happens. The inner call(shell-command "id")is never visited.Verified bypass:
The same pattern applies to
let*,cl-loop,dotimes, and any macro form that wraps dangerous calls in a binding position rather than a tail position.Bypass 2: Dynamic function-name construction via
funcall/apply+internThe checker operates on static symbols. A function name constructed at runtime is invisible to it:
Any variant that avoids placing the dangerous symbol in a statically-visible position evades the check.
Impact
Both bypasses require the attacker to have already gained
eval-elisptool access (which is itself gated bydestructiveHintand the function-level blocklist). They are therefore a second layer of concern, but relevant when users place trust in the Emacs-side enforcement.Proposed approaches
For bypass 1
Run
macroexpand-allon the form before walking it. This convertslet,let*,when,unless,dolist, etc. into their primitive equivalents, making all subforms visible to the walker. Caveats: adds latency for complex forms; user-defined macros may not expand predictably.For bypass 2
The static checker cannot close this gap by design — it cannot evaluate arbitrary code to determine what
(intern ...)will return. Options:funcall,apply, andinterntomcp-server-security-dangerous-functionsby default, so they are blocked unless explicitly allowed. This is a breaking change for legitimate use.Related
with-temp-file,write-file,append-to-file,make-network-process,open-network-stream,directory-files,insert-file-contents-literally), glob pattern support inmcp-server-security-sensitive-file-patterns.