CVE-2023-50715: Insufficient Information (NVD-CWE-noinfo)
value: 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.

When 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.

However, 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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 4.3
Published: 2023-12-15T03:15Z
Repository: https://github.com/home-assistant/core
Language: Python
Files changed: homeassistant/components/auth/login_flow.py, homeassistant/components/person/__init__.py, tests/components/auth/test_login_flow.py, tests/components/person/test_init.py
Commit date: 2023-12-14 20:36:59 +0100
Commit message: Disable user profiles on login screen (#105749)


Diff: diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py
index 96255f59c7..9b96e57dbd 100644
--- a/homeassistant/components/auth/login_flow.py
+++ b/homeassistant/components/auth/login_flow.py
@@ -89,11 +89,10 @@ from homeassistant.components.http.ban import (
 )
 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
 
 if TYPE_CHECKING:
     from homeassistant.auth.providers.trusted_networks import (
@@ -163,12 +162,10 @@ class AuthProvidersView(HomeAssistantView):
 
         cloud_connection = is_cloud_connection(hass)
 
         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
                     continue
 
@@ -177,34 +174,16 @@ class AuthProvidersView(HomeAssistantView):
                         remote_address
                     )
                 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,
                 }
             )
 
         return self.json(providers)
 
diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py
index b6f8b5b2db..c796cb8d84 100644
--- a/homeassistant/components/person/__init__.py
+++ b/homeassistant/components/person/__init__.py
@@ -1,10 +1,9 @@
 """Support for tracking people."""
 from __future__ import annotations
 
 from http import HTTPStatus
-from ipaddress import ip_address
 import logging
 from typing import Any
 
 from aiohttp import web
 import voluptuous as vol
@@ -49,16 +48,14 @@ from homeassistant.helpers import (
     entity_registry as er,
     service,
 )
 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__)
 
 ATTR_SOURCE = "source"
 ATTR_USER_ID = "user_id"
@@ -586,35 +583,10 @@ class ListPersonsView(HomeAssistantView):
     url = "/api/person/list"
     name = "api:person:list"
 
     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",
         )
diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py
index 639bbb9a9c..27652ca2be 100644
--- a/tests/components/auth/test_login_flow.py
+++ b/tests/components/auth/test_login_flow.py
@@ -1,14 +1,12 @@
 """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
 
 from . import BASE_CONFIG, async_setup_auth
 
@@ -65,26 +63,20 @@ async def test_fetch_auth_providers(
 
 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")
     assert resp.status == HTTPStatus.OK
     assert await resp.json() == [expected]
@@ -103,13 +95,11 @@ async def test_fetch_auth_providers_home_assistant_person_not_loaded(
     hass: HomeAssistant,
     aiohttp_client: ClientSessionGenerator,
     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(
     ("ip", "is_local"),
     [
@@ -132,11 +122,10 @@ async def test_fetch_auth_providers_home_assistant_person_loaded(
 
     await _test_fetch_auth_providers_home_assistant(
         hass,
         aiohttp_client,
         ip,
-        lambda user: {"users": {user.id: user.name}} if is_local else {},
     )
 
 
 async def test_fetch_auth_providers_onboarding(
     hass: HomeAssistant, aiohttp_client: ClientSessionGenerator
diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py
index 4d7781a095..1866f682b5 100644
--- a/tests/components/person/test_init.py
+++ b/tests/components/person/test_init.py
@@ -1,7 +1,6 @@
 """The tests for the person component."""
-from collections.abc import Callable
 from http import HTTPStatus
 from typing import Any
 from unittest.mock import patch
 
 import pytest
@@ -29,11 +28,10 @@ from homeassistant.helpers import entity_registry as er
 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
 
 
 async def test_minimal_setup(hass: HomeAssistant) -> None:
     """Test minimal config with only name."""
@@ -850,46 +848,14 @@ async def test_entities_in_person(hass: HomeAssistant) -> None:
         "device_tracker.paulus_iphone",
         "device_tracker.paulus_ipad",
     ]
 
 
-@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."""
 
     user_id = hass_admin_user.id
     admin = {"id": "1234", "name": "Admin", "user_id": user_id, "picture": "/bla"}
@@ -900,13 +866,12 @@ 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"}

Vulnerable code: from homeassistant.util.network import is_local
            additional_data = {}

            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
                    )
                }
                    **additional_data,
from ipaddress import ip_address
from homeassistant.helpers.network import is_cloud_connection
from homeassistant.util.network import is_local
        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)
            }
from collections.abc import Callable
from homeassistant.auth.models import User
    additional_expected_fn: Callable[[User], dict[str, Any]],
    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)

        **additional_expected_fn(user),
    await _test_fetch_auth_providers_home_assistant(
        hass, aiohttp_client, ip, lambda _: {}
    )
        lambda user: {"users": {user.id: user.name}} if is_local else {},
from collections.abc import Callable
from tests.test_util import mock_real_ip
@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"},
        ),
    ],
)
    ip: str,
    status_code: HTTPStatus,
    expected_fn: Callable[[dict[str, Any]], dict[str, Any]],
    mock_real_ip(hass.http.app)(ip)
    assert resp.status == status_code
    assert result == expected_fn(admin)
Fixed code:         return self.json_message(
            message="Not local",
            status_code=HTTPStatus.BAD_REQUEST,
            message_code="not_local",
    await _test_fetch_auth_providers_home_assistant(hass, aiohttp_client, ip)
    assert resp.status == HTTPStatus.BAD_REQUEST
    assert result == {"code": "not_local", "message": "Not local"}