Commit b3f2089
fix(code-mode): close proxy-laundering, ..-traversal, and 5 other Patch76 review findings
Implements the 9-item plan in
#854 (comment)
addressing the CHANGES_REQUESTED review at
#854 (review)
on commit 11ba402.
Blocker 1 — recursive-self-call guard bypassable via ha_call_write_tool
-----------------------------------------------------------------------
Two-layer fix:
* ``CategorizedSearchTransform`` gains an ``enable_code_mode: bool``
constructor parameter (default False, preserves prior behaviour for
installations that aren't running code mode). When True,
``_rebuild_category_cache`` swaps ``get_tool_catalog(ctx)`` for
``_get_visible_tools(ctx)`` — the same FastMCP helper that
``BM25SearchTransform`` already uses. Pinned tools (including
``ha_manage_custom_tool``) drop out of ``_read_tools`` /
``_write_tools`` / ``_delete_tools``, so ``categorized_call`` for a
pinned name falls through to the ``RESOURCE_NOT_FOUND`` branch
rather than dispatching the underlying tool. ``server.py`` flips the
flag on whenever ``settings.enable_code_mode`` is True.
* ``_BLOCKED_TOOLS`` (sandbox-side defense in depth) now also includes
the four search-transform synthetics: ``ha_search_tools``,
``ha_call_read_tool``, ``ha_call_write_tool``, ``ha_call_delete_tool``.
Even if a future regression re-enables the proxy dispatch, sandbox
code can't reach the laundering path. Direct calls to underlying
tools by their real name (``call_tool("ha_get_history", ...)``) keep
working — the block is on the synthetics only, so individual
underlying tools can still be denylisted in the future without
needing to rework the proxy.
Blocker 2 — ``..`` traversal in ``_normalize_endpoint``
-------------------------------------------------------
httpx happily resolves ``base_url='http://ha:8123/api'`` +
``../auth/providers`` to ``http://ha:8123/auth/providers``, escaping
the ``/api/`` prefix. After ``lstrip("/")`` and the optional ``api/``
strip, ``_normalize_endpoint`` now splits on ``/`` and rejects any
segment exactly equal to ``..``. ``..bar`` (filename starting with
two dots) and ``...`` (three dots) stay allowed — they aren't
traversal segments, just unusual filenames.
Verified against 12 endpoint cases covering all 4 traversal patterns
plus prior security guards (absolute URL, protocol-relative,
userinfo) still holding.
Additional registry blocks (``_BLOCKED_WS_COMMANDS``)
-----------------------------------------------------
Added 9 entries for floor / label / category registry mutations to
match the existing area / device / entity coverage. Each has a
wrapping MCP tool (``ha_config_set_floor`` / ``ha_config_set_label`` /
``ha_config_set_category``) so the same "force through the validated
path" rationale applies.
Additional event blocks (``_BLOCKED_HA_INTERNAL_EVENTS``)
---------------------------------------------------------
Added ``script_finished`` (pairs with the existing ``script_started``
block) and ``logbook_entry`` (this event IS the documented logbook
write API; spoofing injects fabricated rows directly into the user's
primary investigation tool — data-integrity issue, not just attack
surface). ``automation_triggered`` and ``call_service`` stay allowed
— legit "verify my handler reacts" use cases and downstream
consumers can already check event context for provenance.
``list_saved`` shape — nest under ``data.saved_tools``
------------------------------------------------------
``_SAVE_NAME_PATTERN`` accepts every key the *other* response shapes
use (``result``, ``code``, ``justification``, ``saved_tool``, ``count``).
A consumer doing ``r["data"]["result"]`` after a list_saved call
would have gotten a saved-tool entry instead of a run-result. Fixed
by nesting the dict under a stable ``saved_tools`` key. Updated
``test_list_saved_tools`` to pin the new shape.
Log injection (``%s`` interpolation of LLM-controlled strings)
--------------------------------------------------------------
Added ``_log_safe`` helper that replaces ``\r`` / ``\n`` / ``\t``
with spaces and truncates to 200 chars. Applied to the
``ha_manage_custom_tool invoked — justification: %s`` log line at
``tools_code.py:1151`` so an LLM-supplied
``"real reason\nFAKE_CRITICAL: …"`` cannot inject a synthetic second
log line. Audit-log endpoint/type fields use ``%r`` already which
escapes via repr(); no change needed there. The DEBUG-level
``code:\n%s`` line stays raw — multi-line is intentional (operator's
primary forensic artefact) and the format string already opens a
fresh line so there's nothing to inject into.
Tests
-----
* ``TestCodeModeAdditionalResourceLimits`` (3 tests) —
memory / recursion / invocation-cap. Previous suite only covered
timeout.
* ``TestCodeModeNormalizeEndpointTraversal`` (1 parametrized test, 4
rows) — all 4 ``..`` traversal patterns reject.
* ``TestCodeModeProxyLaunderingBlocked`` (1 parametrized test) —
``call_tool`` to each of the 4 search synthetics returns the
``AUTH_INSUFFICIENT_PERMISSIONS`` block.
* ``test_list_saved_tools`` updated to verify new
``data.saved_tools[name]`` + ``data.count`` shape.
* ``test_save_warning_rollback_shape`` (skipped) — placeholder noting
that the unit suite covers the persistence-failure rollback;
E2E coverage requires runtime filesystem poisoning the addon
container model doesn't expose.
Lint/mypy/ast-grep all clean. 24 unit tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 3b43cc3 commit b3f2089
4 files changed
Lines changed: 403 additions & 22 deletions
File tree
- src/ha_mcp
- tools
- transforms
- tests/src/e2e/tools
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
547 | 547 | | |
548 | 548 | | |
549 | 549 | | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
550 | 556 | | |
551 | 557 | | |
552 | 558 | | |
553 | | - | |
| 559 | + | |
554 | 560 | | |
555 | 561 | | |
| 562 | + | |
556 | 563 | | |
557 | 564 | | |
558 | 565 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
50 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
51 | 88 | | |
52 | 89 | | |
53 | 90 | | |
| |||
99 | 136 | | |
100 | 137 | | |
101 | 138 | | |
| 139 | + | |
102 | 140 | | |
103 | 141 | | |
104 | 142 | | |
| |||
111 | 149 | | |
112 | 150 | | |
113 | 151 | | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
114 | 157 | | |
115 | 158 | | |
116 | 159 | | |
| |||
139 | 182 | | |
140 | 183 | | |
141 | 184 | | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
142 | 199 | | |
143 | 200 | | |
144 | 201 | | |
| |||
578 | 635 | | |
579 | 636 | | |
580 | 637 | | |
581 | | - | |
582 | | - | |
583 | | - | |
584 | | - | |
585 | | - | |
586 | | - | |
587 | | - | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
588 | 651 | | |
589 | 652 | | |
590 | 653 | | |
| |||
601 | 664 | | |
602 | 665 | | |
603 | 666 | | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
604 | 677 | | |
605 | 678 | | |
606 | 679 | | |
| |||
992 | 1065 | | |
993 | 1066 | | |
994 | 1067 | | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
995 | 1076 | | |
996 | 1077 | | |
997 | 1078 | | |
998 | | - | |
999 | | - | |
1000 | | - | |
1001 | | - | |
1002 | | - | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
1003 | 1087 | | |
1004 | | - | |
1005 | 1088 | | |
1006 | 1089 | | |
1007 | 1090 | | |
| |||
1086 | 1169 | | |
1087 | 1170 | | |
1088 | 1171 | | |
1089 | | - | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
1090 | 1181 | | |
1091 | 1182 | | |
1092 | 1183 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
166 | 166 | | |
167 | 167 | | |
168 | 168 | | |
| 169 | + | |
169 | 170 | | |
170 | 171 | | |
171 | 172 | | |
| |||
183 | 184 | | |
184 | 185 | | |
185 | 186 | | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
186 | 196 | | |
187 | 197 | | |
188 | 198 | | |
| |||
201 | 211 | | |
202 | 212 | | |
203 | 213 | | |
204 | | - | |
205 | | - | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
206 | 229 | | |
207 | 230 | | |
208 | 231 | | |
| |||
0 commit comments