Commit cff4c3f
harden(handlers): route every path-taking handler through McpPathValidator (#546)
* harden(handlers): route every path-taking handler through McpPathValidator
The strong traversal validator from #347 was wired into only filesystem and
script handlers; ~12 other path-taking handlers used a bare begins_with("res://")
check (which does not reject "..") or no check at all (relying solely on
ResourceLoader.exists/load). This unifies all of them on McpPathValidator and
extends the validator with two checks.
Validator (utils/path_validator.gd):
- Reject embedded null bytes (truncation trap — the path written could differ
from the one validated).
- New for_write flag: write callers additionally refuse res://project.godot,
the res://.godot/ metadata dir, and .import sidecars (overwriting these
corrupts the project / import cache). Reads still permit inspecting them.
Signature is backward-compatible (for_write defaults to false).
Writers now validate with for_write=true (closes the traversal-write primitive):
resource_io.save_to_disk (backs resource/environment/texture/curve saves),
scene create_scene/save_scene_as, material/theme save helpers, filesystem
write_text, script create/patch.
Load/read sites now validate (closes the unvalidated-load surface, incl. the
@tool-script-execution risk from open_scene/create_node):
resource load/assign + nested object-property loads, scene open_scene,
script attach_script, node create_node scene_path + set_property resource
values, audio set_stream + list root, material assign/shader/list,
ui theme + stylebox, autoload path, curve set_points, particle mesh/
material/texture, material_values.load_texture.
Path-validation rejections report VALUE_OUT_OF_RANGE (the code these handlers
already used for "must start with res://"), keeping the INVALID_PARAMS catch-all
count under the audit-v2 #21 ceiling enforced by test_error_code_distribution.
The four pre-existing validator sites that already wrapped errors as
INVALID_PARAMS (filesystem, script create/read/patch/find, resource_io save,
material _validate_material_path) are left unchanged.
set_stream and the other load handlers now reject user:// (consistent with the
validator's existing test_user_prefix_rejected policy); the audio test fixture
moves from user:// to a res:// .tres registered via EditorFileSystem.update_file.
Tests (run against live Godot 4.6.3): validator unit tests for null-byte +
write blocklist + read-allows; scene traversal/manifest-overwrite rejection.
All affected suites pass (path_validator/scene/resource/node/material/theme/
curve/particle/ui/audio/autoload/filesystem/script). test_error_code_distribution
passes. Parse check clean.
Addresses advisory GHSA-p5x8-v25q-qw69 (GH-1, GH-2, GH-3, GH-4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harden(handlers): address review — uid:// loads, case-fold blocklist, shared helper
Follow-up to the McpPathValidator unification, fixing the code-review findings.
Correctness:
- Restore uid:// and user:// support on load handlers. ResourceLoader accepts
both (uid:// is an opaque resource id that can't express traversal; user://
is the user data sandbox), but routing every load through the strict res://
validator rejected them — a regression for uid:// refs copied from .tscn/.uid
and for user:// runtime assets. New validate_loadable_path() accepts res://
(confined), uid:// (as-is), and user:// (confined under the user root), and
load sites now use it: set_property/assign_resource/resource property dicts,
open_scene, create_node, attach_script, set_stream, particle mesh/material/
texture, material assign/shader, ui theme/stylebox, curve, material_values.
- Case-fold the write blocklist. macOS (APFS) and Windows (NTFS) are
case-insensitive by default, so res://Project.godot resolved to the real
project.godot and slipped past a case-sensitive compare.
- Add override.cfg to the write blocklist (applied over project.godot at
startup — same takeover surface as the manifest).
- Reads no longer run the write blocklist: _validate_material_path /
_validate_res_path / _load_material_from_path / _load_theme_from_params take
for_write, passed true only by the create/save callers. get_material and
apply_theme (pure reads) no longer return a spurious "Refusing to write".
- Stop blocking .import writes — editing import config then reimporting is a
legitimate, recoverable workflow; the blocklist is the startup-execution
surface (manifest, override.cfg, .godot/) only.
Cleanup / altitude:
- Single error-code choke point: McpPathValidator.path_error / loadable_error
return a ready error dict (MISSING_REQUIRED_PARAM for empty, VALUE_OUT_OF_RANGE
for invalid) so every handler reports the same code for the same failure.
filesystem/script move off their old INVALID_PARAMS wrapping onto this.
- curve set_points validates the load path (the save layer, resource_io.save_to_disk,
remains the authoritative write-confinement check) instead of double-running
the write validator.
- Drop the redundant is_empty() pre-check in material_values.load_texture.
- Document the lexical-containment (no symlink resolution) limitation in the
validator — GDScript has no realpath; the loopback boundary is the control.
Tests (live Godot 4.6.3, all suites 0 failures): uid:// / user:// acceptance,
user:// traversal + unknown-scheme rejection, case-insensitive blocklist,
override.cfg block, .import now allowed; audio fixture moved back to user://
(no repo/EFS pollution). test_path_traversal_guard counts any McpPathValidator
delegation; missing-path tests now assert MISSING_REQUIRED_PARAM.
Addresses review findings 1-10 on GHSA-p5x8-v25q-qw69 (path confinement).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harden(handlers): address Copilot review on path validation
- Guard the null-byte check against an empty String.chr(0) sentinel in both
validate_resource_path and validate_loadable_path. On builds that normalize
embedded nulls away (e.g. 4.3), contains("") would be true and reject every
path; a String that can't hold a null can't smuggle one, so skip the check.
- Update stale "res:// path" comments in ui_handler (stylebox override) and
material_values.load_texture — both now accept uid:// / user:// via
validate_loadable_path.
- Tighten test_path_traversal_guard: assert attach_script is present and require
>=5 McpPathValidator delegations in script_handler, so a regression where
attach_script stops validating its path is caught.
Live Godot 4.6.3: path_validator/filesystem/script/ui/material/resource/scene/
audio suites all pass; test_path_traversal_guard green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: wrap long func-name tuple in test_path_traversal_guard (ruff E501)
The 5-entry func list exceeded the 100-char line limit after adding
attach_script. Pure formatting; the assertion is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(handlers): align remaining res:// messages with uid:// / user:// support
Copilot re-review: now that the theme / stylebox / shader load paths accept
uid:// and user:// via loadable_error, the surrounding comments, the
build_layout docstring, the stylebox fallback error ("expects a res:// path"),
and the shader_path missing-arg error still said res:// only. Text-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent c0e3be7 commit cff4c3f
21 files changed
Lines changed: 405 additions & 108 deletions
File tree
- plugin/addons/godot_ai
- handlers
- utils
- test_project/tests
- tests/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
104 | 108 | | |
105 | 109 | | |
106 | 110 | | |
| |||
259 | 263 | | |
260 | 264 | | |
261 | 265 | | |
262 | | - | |
263 | | - | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
264 | 269 | | |
265 | 270 | | |
266 | 271 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
38 | 39 | | |
39 | 40 | | |
40 | 41 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
41 | 44 | | |
42 | 45 | | |
43 | 46 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
13 | | - | |
14 | | - | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
40 | | - | |
41 | | - | |
42 | | - | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| |||
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
68 | | - | |
| 68 | + | |
69 | 69 | | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
70 | 73 | | |
71 | 74 | | |
72 | 75 | | |
| |||
111 | 114 | | |
112 | 115 | | |
113 | 116 | | |
114 | | - | |
| 117 | + | |
115 | 118 | | |
116 | 119 | | |
117 | 120 | | |
| |||
169 | 172 | | |
170 | 173 | | |
171 | 174 | | |
172 | | - | |
| 175 | + | |
173 | 176 | | |
174 | 177 | | |
175 | 178 | | |
| |||
297 | 300 | | |
298 | 301 | | |
299 | 302 | | |
300 | | - | |
301 | | - | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
302 | 306 | | |
303 | 307 | | |
304 | 308 | | |
| |||
366 | 370 | | |
367 | 371 | | |
368 | 372 | | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
369 | 376 | | |
370 | 377 | | |
371 | 378 | | |
| |||
463 | 470 | | |
464 | 471 | | |
465 | 472 | | |
466 | | - | |
| 473 | + | |
467 | 474 | | |
468 | 475 | | |
469 | 476 | | |
| |||
564 | 571 | | |
565 | 572 | | |
566 | 573 | | |
567 | | - | |
| 574 | + | |
568 | 575 | | |
569 | 576 | | |
570 | 577 | | |
| |||
665 | 672 | | |
666 | 673 | | |
667 | 674 | | |
668 | | - | |
| 675 | + | |
669 | 676 | | |
670 | 677 | | |
671 | | - | |
672 | | - | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
673 | 681 | | |
674 | 682 | | |
675 | 683 | | |
| |||
683 | 691 | | |
684 | 692 | | |
685 | 693 | | |
686 | | - | |
687 | | - | |
| 694 | + | |
| 695 | + | |
688 | 696 | | |
689 | 697 | | |
690 | 698 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
156 | | - | |
| 156 | + | |
| 157 | + | |
157 | 158 | | |
158 | | - | |
| 159 | + | |
159 | 160 | | |
160 | 161 | | |
161 | 162 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
43 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| |||
222 | 223 | | |
223 | 224 | | |
224 | 225 | | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
225 | 229 | | |
226 | 230 | | |
227 | 231 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
341 | 341 | | |
342 | 342 | | |
343 | 343 | | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
344 | 347 | | |
345 | 348 | | |
346 | 349 | | |
| |||
357 | 360 | | |
358 | 361 | | |
359 | 362 | | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
360 | 366 | | |
361 | 367 | | |
362 | 368 | | |
| |||
407 | 413 | | |
408 | 414 | | |
409 | 415 | | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
410 | 419 | | |
411 | 420 | | |
412 | 421 | | |
| |||
417 | 426 | | |
418 | 427 | | |
419 | 428 | | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
420 | 432 | | |
421 | 433 | | |
422 | 434 | | |
| |||
447 | 459 | | |
448 | 460 | | |
449 | 461 | | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
450 | 465 | | |
451 | 466 | | |
452 | 467 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
67 | | - | |
68 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
69 | 70 | | |
70 | 71 | | |
71 | 72 | | |
| |||
112 | 113 | | |
113 | 114 | | |
114 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
115 | 120 | | |
116 | 121 | | |
117 | 122 | | |
| |||
248 | 253 | | |
249 | 254 | | |
250 | 255 | | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
251 | 259 | | |
252 | 260 | | |
253 | 261 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
94 | | - | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
95 | 96 | | |
96 | 97 | | |
97 | 98 | | |
| |||
148 | 149 | | |
149 | 150 | | |
150 | 151 | | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
151 | 156 | | |
152 | 157 | | |
153 | 158 | | |
| |||
202 | 207 | | |
203 | 208 | | |
204 | 209 | | |
205 | | - | |
206 | | - | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
207 | 213 | | |
208 | 214 | | |
209 | 215 | | |
| |||
0 commit comments