CVE-2023-50715 (Python, published 2023-12-15T03:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: 4.3)
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/home-assistant/core
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 message: Disable user profiles on login screen (#105749)



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"}