Repository navigation
v2.26.6
Findings of a full code audit, each fixed with a test that failed before.
Security
create_helper/update_helpercould run any WebSocket command as the calling admin. The helper'sconfigwas spread into the WebSocket message after itstype, so atypekey in the config replaced the command. That allowed any service call, bypassing the deliberately narrow service tools (#76), and could mint a long-lived access token (verified: it returned one). That token outlives the arm window.idandtypecan no longer be passed as command arguments.- How to check whether it was used: in your HA profile, under Security, look for long-lived access tokens you didn't create, and delete them. With mirroring on, the mirror repo's
.storage/<domain>history shows every helper write.
- How to check whether it was used: in your HA profile, under Security, look for long-lived access tokens you didn't create, and delete them. With mirroring on, the mirror repo's
- Tool arguments were never checked against their schema. Home Assistant passes the client's JSON straight to a tool, so the declared types, ranges and required fields weren't enforced. Examples:
get_logslinesabove 1000, alist_mqtt_topicslisten beyond its 10 s cap, a string where a list belongs (iterated character by character), or an unknown key crashing a tool. Every call is now validated first, with the schema's defaults filled in, and an invalid one returnserror_type: "Invalid". A tool error that escapes is now returned as a tool error and logged, rather than raised raw. packagecould point a write at another allowlisted file.write_automation/write_script/create_template_entityjoined it aspackages/<package>, and the path check normalised..away. Withpackage="../scripts.yaml", an automation was written intoscripts.yamlas a script namedautomation.packagemust now be a.yamlpath insidepackages/. File paths with a..component are refused outright.- A symlink could lead around the denylist. The denylist was matched against a path's name, not against where it leads. A link inside
packages/tosecrets.yamlwas readable. It's now also matched against the resolved path. Containment in the config folder is now a path check, not a string prefix, so/configno longer also matches/config_old. - The mirror token was sent to the browser. The options form pre-filled the token field, and a form's defaults are sent to the frontend, password field or not. The field is now left empty, and leaving it empty keeps the stored token. The mirror repository must be in
owner/repoform. - A future time in the arm file lifted the 4-hour cap. The cap counts from the time written in the file, so a mistyped or forward-dated one kept dev_tools armed for as long as it stayed in use. A time more than a minute ahead now counts as not armed.
get_rest_commandreturned literal credentials. Passwords, API keys,Bearertokens and URL passwords came back as written, even thoughget_config_filewithholds the same block (#105). They're now masked, and!secretreferences are shown as before. The same gap inget_automation/get_scriptis tracked in #151: those feed write round-trips, so masking needs a decision first.- Hardening:
- Mirror paths are percent-encoded in GitHub API URLs, and a
./..segment is refused. get_addon_logsonly takes an add-on slug.- The credential checks also recognise
passphrase,psk,encryption_key,network_keyand header spellings likeX-Api-Key. - An inactive user is never resolved as the caller.
- Mirror paths are percent-encoded in GitHub API URLs, and a
Fixed
get_logsignoredsince/until, which its description offers. They're now applied, as ISO 8601 times (a time without an offset is in HA's time zone). Log times are timezone-aware, andlinesno longer has a default that would overrideoffset/limitpaging.- Tools say which integration provides them (
Tool.integration), which HA 2026.10 logs a warning about otherwise. Returningllm.ToolResult(HA 2026.10's other deprecation, breaking in 2027.11) is tracked in #152.