-
-
Notifications
You must be signed in to change notification settings - Fork 37.7k
Fix hassio auth IndexError on Supervisor Unix socket requests #169911
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
8ee1df9
734305a
bac202a
1f946ea
d36e13f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |||||||||||||||||||||||||||||||||
| 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 @@ def __init__(self, hass: HomeAssistant, user: User) -> None: | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+56
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I'd overprefer a walrus operator here to patch out the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that the assert is existing code.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True, merely spotted a potential improvement. :)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use the same pattern (assert) also in other places in the hassio integration. From what I understand that happens if the connection closed. I am not sure how realistic this is in this particular case, the close must have happened between the request coming in and processing here 🤔 I'd rather prefer to address all sites at once, if we decide to change how to handle
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense. I agree this does not need to block this PR, especially if the same pattern exists elsewhere.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It got added with #74603 as a mypy defense, so I assume the latter. |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Check caller token | ||||||||||||||||||||||||||||||||||
| if request[KEY_HASS_USER].id != self.user.id: | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,17 @@ | ||
| """The tests for the hassio component.""" | ||
|
|
||
| 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.components.http import KEY_HASS_USER | ||
| from homeassistant.core import HomeAssistant | ||
|
|
||
|
|
||
| async def test_auth_success(hassio_client_supervisor: TestClient) -> None: | ||
|
|
@@ -162,6 +168,46 @@ async def test_password_fails_no_auth(hassio_noauth_client: TestClient) -> None: | |
| assert resp.status == HTTPStatus.UNAUTHORIZED | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| ("peername", "unix_socket"), | ||
| [ | ||
| # Unix socket transports report an empty string for peername. Before | ||
| # the fix this raised IndexError on `peername[0]`. | ||
| ("", True), | ||
| # Defensive: a TCP transport with no peername at all should be | ||
| # rejected, not crash. | ||
| (None, False), | ||
| ], | ||
| ) | ||
| async def test_check_access_unix_socket_or_missing_peername( | ||
| hass: HomeAssistant, | ||
| hassio_stubs: None, | ||
| peername: str | None, | ||
| unix_socket: bool, | ||
| ) -> 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__.side_effect = lambda key: user if key is KEY_HASS_USER else None | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider moving the side effect to a test parameter that is a lambda. |
||
|
|
||
| with patch( | ||
| "homeassistant.components.hassio.auth.is_supervisor_unix_socket_request", | ||
| return_value=unix_socket, | ||
| ): | ||
| if unix_socket: | ||
| auth_view._check_access(request) | ||
| else: | ||
| with pytest.raises(HTTPUnauthorized): | ||
|
MartinHjelmare marked this conversation as resolved.
Outdated
|
||
| 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( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.