From a4227ef1bcd1cbf08fd96f2ae74976b34abbebc0 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 6 May 2026 17:48:35 +0200 Subject: [PATCH] Fix hassio auth IndexError on Supervisor Unix socket requests (#169911) --- homeassistant/components/hassio/auth.py | 21 ++++++----- tests/components/hassio/test_auth.py | 47 ++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 8589bc0f1347..9c9d7cc710ef 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, HomeAssistantView +from homeassistant.components.http.const import is_supervisor_unix_socket_request from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -41,14 +42,18 @@ class HassIOBaseAuth(HomeAssistantView): def _check_access(self, request: web.Request) -> None: """Check if this call is from Supervisor.""" - # Check caller IP - hassio_ip = os.environ["SUPERVISOR"].split(":")[0] - assert request.transport - if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address( - hassio_ip - ): - _LOGGER.error("Invalid auth request from %s", request.remote) - raise HTTPUnauthorized + # Requests over the Supervisor Unix socket are authenticated by the + # http auth middleware as the Supervisor user, so the caller-IP check + # below does not apply (and would crash, since `peername` is empty for + # Unix sockets). The user-ID check still runs to ensure only the + # Supervisor user can reach this endpoint. + if not is_supervisor_unix_socket_request(request): + hassio_ip = os.environ["SUPERVISOR"].split(":")[0] + assert request.transport + peername = request.transport.get_extra_info("peername") + if not peername or ip_address(peername[0]) != ip_address(hassio_ip): + _LOGGER.error("Invalid auth request from %s", request.remote) + raise HTTPUnauthorized # Check caller token if request[KEY_HASS_USER].id != self.user.id: diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index ad96b58e99db..7f5e4ba331b6 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,11 +1,17 @@ """The tests for the hassio component.""" +from contextlib import AbstractContextManager, ExitStack as DefaultContext from http import HTTPStatus -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch from aiohttp.test_utils import TestClient +from aiohttp.web_exceptions import HTTPUnauthorized +import pytest from homeassistant.auth.providers.homeassistant import InvalidAuth +from homeassistant.components.hassio.auth import HassIOBaseAuth +from homeassistant.components.hassio.const import DATA_CONFIG_STORE +from homeassistant.core import HomeAssistant async def test_auth_success(hassio_client_supervisor: TestClient) -> None: @@ -162,6 +168,45 @@ async def test_password_fails_no_auth(hassio_noauth_client: TestClient) -> None: assert resp.status == HTTPStatus.UNAUTHORIZED +@pytest.mark.parametrize( + ("peername", "unix_socket", "expectation"), + [ + # Unix socket transports report an empty string for peername. Before + # the fix this raised IndexError on `peername[0]`. + ("", True, DefaultContext()), + # Defensive: a TCP transport with no peername at all should be + # rejected, not crash. + (None, False, pytest.raises(HTTPUnauthorized)), + ], +) +@pytest.mark.usefixtures("hassio_stubs") +async def test_check_access_unix_socket_or_missing_peername( + hass: HomeAssistant, + peername: str | None, + unix_socket: bool, + expectation: AbstractContextManager, +) -> None: + """Test _check_access handles Unix socket requests and missing peername.""" + hassio_user_id = hass.data[DATA_CONFIG_STORE].data.hassio_user + assert hassio_user_id is not None + user = await hass.auth.async_get_user(hassio_user_id) + assert user is not None + + auth_view = HassIOBaseAuth(hass, user) + request = MagicMock() + request.transport.get_extra_info.return_value = peername + request.__getitem__.return_value = user + + with ( + patch( + "homeassistant.components.hassio.auth.is_supervisor_unix_socket_request", + return_value=unix_socket, + ), + expectation, + ): + auth_view._check_access(request) + + async def test_password_no_user(hassio_client_supervisor: TestClient) -> None: """Test changing password for invalid user.""" resp = await hassio_client_supervisor.post(