CVE-2023-50715 (published 2023-12-15T03:15Z)

Home Assistant is open source home automation software. Prior to version 2023.12.3, the login page discloses all active user accounts to any unauthenticated browsing request originating on the Local Area Network. Version 2023.12.3 contains a patch for this issue.\n\nWhen starting the Home Assistant 2023.12 release, the login page returns all currently active user accounts to browsing requests from the Local Area Network. Tests showed that this occurs when the request is not authenticated and the request originated locally, meaning on the Home Assistant host local subnet or any other private subnet. The rationale behind this is to make the login more user-friendly and an experience better aligned with other applications that have multiple user-profiles.\n\nHowever, as a result, all accounts are displayed regardless of them having logged in or not and for any device that navigates to the server. This disclosure is mitigated by the fact that it only occurs for requests originating from a LAN address. But note that this applies to the local subnet where Home Assistant resides and to any private subnet that can reach it.

CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/home-assistant/core
commit_details: repo_url: https://github.com/home-assistant/core
author: Franck Nijhof
author_date: 2023-12-14 20:28:08+01:00
msg: Disable user profiles on login screen (#105749)
repository: repo_url: https://github.com/home-assistant/core
repo_name: home-assistant/core
description: :house_with_garden: Open source home automation that puts local control and privacy first.
repo_language: Python
owner: home-assistant
file_changes: filename: login_flow.py
change_type: ModificationType.MODIFY
diff: @@ -91,7 +91,6 @@ from homeassistant.components.http.data_validator import RequestDataValidator
 from homeassistant.components.http.view import HomeAssistantView
 from homeassistant.core import HomeAssistant
 from homeassistant.helpers.network import is_cloud_connection
-from homeassistant.util.network import is_local
 
 from . import indieauth
 
@@ -165,8 +164,6 @@ class AuthProvidersView(HomeAssistantView):
 
         providers = []
         for provider in hass.auth.auth_providers:
-            additional_data = {}
-
             if provider.type == "trusted_networks":
                 if cloud_connection:
                     # Skip quickly as trusted networks are not available on cloud
@@ -179,30 +176,12 @@ class AuthProvidersView(HomeAssistantView):
                 except InvalidAuthError:
                     # Not a trusted network, so we don't expose that trusted_network authenticator is setup
                     continue
-            elif (
-                provider.type == "homeassistant"
-                and not cloud_connection
-                and is_local(remote_address)
-                and "person" in hass.config.components
-            ):
-                # We are local, return user id and username
-                users = await provider.store.async_get_users()
-                additional_data["users"] = {
-                    user.id: credentials.data["username"]
-                    for user in users
-                    for credentials in user.credentials
-                    if (
-                        credentials.auth_provider_type == provider.type
-                        and credentials.auth_provider_id == provider.id
-                    )
-                }
 
             providers.append(
                 {
                     "name": provider.name,
                     "id": provider.id,
                     "type": provider.type,
-                    **additional_data,
                 }
             )
 

programming_language: Python
filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -2,7 +2,6 @@
 from __future__ import annotations
 
 from http import HTTPStatus
-from ipaddress import ip_address
 import logging
 from typing import Any
 
@@ -51,12 +50,10 @@ from homeassistant.helpers import (
 )
 from homeassistant.helpers.entity_component import EntityComponent
 from homeassistant.helpers.event import async_track_state_change_event
-from homeassistant.helpers.network import is_cloud_connection
 from homeassistant.helpers.restore_state import RestoreEntity
 from homeassistant.helpers.storage import Store
 from homeassistant.helpers.typing import ConfigType
 from homeassistant.loader import bind_hass
-from homeassistant.util.network import is_local
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -588,33 +585,8 @@ class ListPersonsView(HomeAssistantView):
 
     async def get(self, request: web.Request) -> web.Response:
         """Return a list of persons if request comes from a local IP."""
-        try:
-            remote_address = ip_address(request.remote)  # type: ignore[arg-type]
-        except ValueError:
-            return self.json_message(
-                message="Invalid remote IP",
-                status_code=HTTPStatus.BAD_REQUEST,
-                message_code="invalid_remote_ip",
-            )
-
-        hass: HomeAssistant = request.app["hass"]
-        if is_cloud_connection(hass) or not is_local(remote_address):
-            return self.json_message(
-                message="Not local",
-                status_code=HTTPStatus.BAD_REQUEST,
-                message_code="not_local",
-            )
-
-        yaml, storage, _ = hass.data[DOMAIN]
-        persons = [*yaml.async_items(), *storage.async_items()]
-
-        return self.json(
-            {
-                person[ATTR_USER_ID]: {
-                    ATTR_NAME: person[ATTR_NAME],
-                    CONF_PICTURE: person.get(CONF_PICTURE),
-                }
-                for person in persons
-                if person.get(ATTR_USER_ID)
-            }
+        return self.json_message(
+            message="Not local",
+            status_code=HTTPStatus.BAD_REQUEST,
+            message_code="not_local",
         )

programming_language: Python
filename: test_login_flow.py
change_type: ModificationType.MODIFY
diff: @@ -1,12 +1,10 @@
 """Tests for the login flow."""
-from collections.abc import Callable
 from http import HTTPStatus
 from typing import Any
 from unittest.mock import patch
 
 import pytest
 
-from homeassistant.auth.models import User
 from homeassistant.core import HomeAssistant
 from homeassistant.setup import async_setup_component
 
@@ -67,22 +65,16 @@ async def _test_fetch_auth_providers_home_assistant(
     hass: HomeAssistant,
     aiohttp_client: ClientSessionGenerator,
     ip: str,
-    additional_expected_fn: Callable[[User], dict[str, Any]],
 ) -> None:
     """Test fetching auth providers for homeassistant auth provider."""
     client = await async_setup_auth(
         hass, aiohttp_client, [{"type": "homeassistant"}], custom_ip=ip
     )
 
-    provider = hass.auth.auth_providers[0]
-    credentials = await provider.async_get_or_create_credentials({"username": "hello"})
-    user = await hass.auth.async_get_or_create_user(credentials)
-
     expected = {
         "name": "Home Assistant Local",
         "type": "homeassistant",
         "id": None,
-        **additional_expected_fn(user),
     }
 
     resp = await client.get("/auth/providers")
@@ -105,9 +97,7 @@ async def test_fetch_auth_providers_home_assistant_person_not_loaded(
     ip: str,
 ) -> None:
     """Test fetching auth providers for homeassistant auth provider, where person integration is not loaded."""
-    await _test_fetch_auth_providers_home_assistant(
-        hass, aiohttp_client, ip, lambda _: {}
-    )
+    await _test_fetch_auth_providers_home_assistant(hass, aiohttp_client, ip)
 
 
 @pytest.mark.parametrize(
@@ -134,7 +124,6 @@ async def test_fetch_auth_providers_home_assistant_person_loaded(
         hass,
         aiohttp_client,
         ip,
-        lambda user: {"users": {user.id: user.name}} if is_local else {},
     )
 
 

programming_language: Python
filename: test_init.py
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,4 @@
 """The tests for the person component."""
-from collections.abc import Callable
 from http import HTTPStatus
 from typing import Any
 from unittest.mock import patch
@@ -31,7 +30,6 @@ from homeassistant.setup import async_setup_component
 from .conftest import DEVICE_TRACKER, DEVICE_TRACKER_2
 
 from tests.common import MockUser, mock_component, mock_restore_cache
-from tests.test_util import mock_real_ip
 from tests.typing import ClientSessionGenerator, WebSocketGenerator
 
 
@@ -852,42 +850,10 @@ async def test_entities_in_person(hass: HomeAssistant) -> None:
     ]
 
 
-@pytest.mark.parametrize(
-    ("ip", "status_code", "expected_fn"),
-    [
-        (
-            "192.168.0.10",
-            HTTPStatus.OK,
-            lambda user: {
-                user["user_id"]: {"name": user["name"], "picture": user["picture"]}
-            },
-        ),
-        (
-            "::ffff:192.168.0.10",
-            HTTPStatus.OK,
-            lambda user: {
-                user["user_id"]: {"name": user["name"], "picture": user["picture"]}
-            },
-        ),
-        (
-            "1.2.3.4",
-            HTTPStatus.BAD_REQUEST,
-            lambda _: {"code": "not_local", "message": "Not local"},
-        ),
-        (
-            "2001:db8::1",
-            HTTPStatus.BAD_REQUEST,
-            lambda _: {"code": "not_local", "message": "Not local"},
-        ),
-    ],
-)
 async def test_list_persons(
     hass: HomeAssistant,
     hass_client_no_auth: ClientSessionGenerator,
     hass_admin_user: MockUser,
-    ip: str,
-    status_code: HTTPStatus,
-    expected_fn: Callable[[dict[str, Any]], dict[str, Any]],
 ) -> None:
     """Test listing persons from a not local ip address."""
 
@@ -902,11 +868,10 @@ async def test_list_persons(
     assert await async_setup_component(hass, DOMAIN, config)
 
     await async_setup_component(hass, "api", {})
-    mock_real_ip(hass.http.app)(ip)
     client = await hass_client_no_auth()
 
     resp = await client.get("/api/person/list")
 
-    assert resp.status == status_code
+    assert resp.status == HTTPStatus.BAD_REQUEST
     result = await resp.json()
-    assert result == expected_fn(admin)
+    assert result == {"code": "not_local", "message": "Not local"}

programming_language: Python