Safe mode: the script-search-path control never fires. The blocked attribute scripts search uses does not exist on any supported Blender version
Safe mode: the script-search-path control never fires. The blocked attribute script_directory does not exist on any supported Blender; the live script_directories collection passes validation
Severity: high (a shipped, opt-in security control is inert on every supported version of Blender, and the live surface it was written to block passes validation)
Affected versions: blender-mcp at commit 5f8ddaf6e987c4aa0c3467fcc548838b28f64477 (default branch, 2026-09-07). The safe-mode validator shipped with this commit; the dead name is present since the table was written.
Summary
With BLENDER_MCP_SAFE_MODE=1, the README promises that every script is checked and that safe mode blocks, among other things, "installing code that keeps running after the script ends" (README line 312). The assignment blocklist in src/blender_mcp/safe_mode.py contains an entry whose intent is exactly this:
_FORBIDDEN_ASSIGN_ATTRS: Final[dict[str, str]] = {
...
"script_directory": "redirects Blender's script search path", # line 359
"use_scripts_auto_execute": "enables automatic execution of embedded scripts", # line 360
}However, script_directory (a singular string property) is the pre-2.90 spelling of the preference. The add-on declares "blender": (3, 0, 0) as its minimum (bundled/addon.py line 33), and on every Blender 3.x, 4.x and 5.x the live API is the plural collection bpy.context.preferences.filepaths.script_directories (a ScriptDirectoryCollection of ScriptDirectory, each with an assignable directory filepath). The blocked name cannot appear in any script a model would actually write against a supported Blender, so this control can never fire, and the live surface passes validation.
Against the Blender 4.1 RNA source (source/blender/makesrna/intern/rna_userdef.cc, blender-v4.1-release):
- line 6924:
RNA_def_property(srna, "script_directories", PROP_COLLECTION, PROP_NONE) - line 6741: the collection's
new()function (ScriptDirectoryCollection.new()) - the entry attribute
directory(PROP_STRING,PROP_DIRPATH), whose own RNA doc reads: "Alternate script path, matching the default layout with sub-directories: startup, add-ons, modules, and presets (requires restart)" - the singular spelling
script_directoryoccurs zero times in the 4.1 RNA
Per the Blender manual, scripts under a registered directory's startup/ run on launch, addons/ are loaded, and modules/ become importable: a registered directory is a code-loading path persisted in the user's saved preferences.
Reproduction (validator only, no Blender needed)
From a checkout of the commit above:
import sys
sys.path.insert(0, "src")
from blender_mcp.safe_mode import is_safe
# The live surface on any supported Blender: create and aim a persistent script directory
print(is_safe(
"import bpy\n"
"sd = bpy.context.preferences.filepaths.script_directories.new()\n"
"sd.name = 'assets'\n"
"sd.directory = '~/.cache/evil_scripts'\n"
))
# -> (True, '') ACCEPTED under safe mode
# Retargeting an existing entry also passes
print(is_safe(
"import bpy\n"
"bpy.context.preferences.filepaths.script_directories[-1].directory = '/srv/evil'\n"
))
# -> (True, '') ACCEPTED
# The dead spelling is what the table blocks
print(is_safe(
"import bpy\n"
"bpy.context.preferences.filepaths.script_directory = '/srv/evil'\n"
))
# -> (False, "line 2: assigning 'script_directory': redirects Blender's script search path")
# The neighbouring entry is correctly named against the live API and fires
print(is_safe(
"import bpy\n"
"bpy.context.preferences.filepaths.use_scripts_auto_execute = True\n"
))
# -> (False, "line 2: assigning 'use_scripts_auto_execute': enables automatic execution of embedded scripts")The acceptance is not PoC-only: the server wires the validator in front of the socket lane exactly as shipped (src/blender_mcp/server.py lines 565-583: if safe_mode_enabled(): validate_code(code) before send_command("execute_code", {"code": code})), so the A1 payload crosses the shipped gate and returns "Code executed successfully".
Why it matters
A registered script directory is saved in the user's preferences and is a code-loading path. A script that safe mode accepted has therefore durably redirected where Blender auto-loads code from: code that runs after the script ends and outside the validator's reach, which is precisely what the safe-mode promise excludes. Given safe mode's own stated threat model (prompt injection via asset text steering the model into hostile scripts), this is a short payload an injected script can use to plant a persistent load path pointed at any directory where attacker-influenced .py files can land (downloads folders, project shares, dependency-confusable module names).
Two bounds, stated honestly:
- Under safe mode the script itself cannot write the payload
.pyfile (file writes are blocked), so the persistence payoff additionally needs attacker-controlled content at a reachable path. The void of the shipped control (a blocklist entry keyed on a name no supported Blender has, while the live API passes) holds regardless of that runtime step. - The preferences-persist and relaunch-scan behavior is Blender-documented and proven from the RNA source, but was not exercised in a live Blender here; the validator acceptance itself is executed and reproducible with the snippet above.
The neighbouring entry proves intent: use_scripts_auto_execute is the live 4.x/5.x name and is correctly blocked, so the table is meant to name real API attributes; script_directory is a dead-name miss, not a policy choice.
Suggested fix
Cover the live surface:
- block reads and writes touching
script_directories(for example addscript_directoriesto the bare-attribute list and abpy.context.preferences.filepaths.script_directoriesprefix to the forbidden bpy paths, so both.new()and.directoryassignment fail); - keep the dead
script_directoryentry if you want belt-and-braces for very old guesses, but it cannot be the only spelling; - consider auditing the remaining forbidden tables against the current
bpyAPI for other dead names; the policy is only as strong as the API names it lists.
Tests to add: validate_code must reject bpy.context.preferences.filepaths.script_directories.new() and ...script_directories[-1].directory = '...'.
Source: ahujasid/blender-mcp