mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 02:24:46 -05:00
sandbox: register_service ownership check (Phase 2)
_handle_register_service now requires the service domain to be one this group owns (same main-side _owned_domains() derivation as the fire_event gate), so a compromised sandbox can no longer squat persistent_notification.* or any unclaimed domain.service. Unowned domains are rejected with a HomeAssistantError → remote-error frame; the existing refuse-to-clobber-an-existing-handler check is kept. Existing register_service tests updated to own their mock domains via a MockConfigEntry(sandbox="built-in").
This commit is contained in:
@@ -520,6 +520,13 @@ class SandboxBridge:
|
||||
sandbox side. Exception translation reuses
|
||||
:func:`_translate_remote_error`.
|
||||
|
||||
The service ``domain`` must be one this group owns (same main-side
|
||||
:meth:`_owned_domains` derivation the fire_event gate uses): a
|
||||
compromised sandbox may not squat ``persistent_notification.*`` or any
|
||||
other ``domain.service`` slot outside the domains main routed to it.
|
||||
An unowned domain is rejected with a :class:`HomeAssistantError` (the
|
||||
channel turns it into a remote-error frame).
|
||||
|
||||
If a service with the same ``(domain, service)`` already exists
|
||||
on main (e.g. the host ``light`` EntityComponent registered
|
||||
``light.turn_on`` for our proxy entities, or another integration
|
||||
@@ -528,6 +535,18 @@ class SandboxBridge:
|
||||
"""
|
||||
domain = msg.domain.lower()
|
||||
service = msg.service.lower()
|
||||
if domain not in self._owned_domains():
|
||||
_LOGGER.warning(
|
||||
"SandboxBridge[%s]: refusing register_service for unowned "
|
||||
"domain %r (%s.%s)",
|
||||
self.group,
|
||||
domain,
|
||||
domain,
|
||||
service,
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
f"register_service: domain {domain!r} not owned by group {self.group!r}"
|
||||
)
|
||||
supports_response = _parse_supports_response(msg.supports_response)
|
||||
if self.hass.services.has_service(domain, service):
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -539,6 +539,9 @@ async def test_register_entity_auto_loads_domain_component(
|
||||
|
||||
async def test_register_service_installs_forwarder(hass: HomeAssistant) -> None:
|
||||
"""A sandbox-registered service appears on main and forwards calls back."""
|
||||
MockConfigEntry(
|
||||
domain="mirror_demo", title="Mirror", sandbox="built-in"
|
||||
).add_to_hass(hass)
|
||||
_bridge, main_channel, sandbox_channel = await _wire(hass)
|
||||
seen_calls: list[pb.CallService] = []
|
||||
|
||||
@@ -583,6 +586,9 @@ async def test_forwarded_context_restores_on_echoed_state(
|
||||
echoing that same context_id, main restores the *original*
|
||||
``user_id`` / ``parent_id`` instead of minting a fresh attribution.
|
||||
"""
|
||||
MockConfigEntry(
|
||||
domain="mirror_demo", title="Mirror", sandbox="built-in"
|
||||
).add_to_hass(hass)
|
||||
_bridge, main_channel, sandbox_channel = await _wire(hass)
|
||||
forwarded_ids: list[str] = []
|
||||
|
||||
@@ -653,6 +659,9 @@ async def test_register_service_skips_existing_handler(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Main already owning ``(domain, service)`` is not clobbered."""
|
||||
MockConfigEntry(
|
||||
domain="mirror_local", title="Mirror", sandbox="built-in"
|
||||
).add_to_hass(hass)
|
||||
_bridge, main_channel, sandbox_channel = await _wire(hass)
|
||||
|
||||
async def _local(_call: Any) -> None:
|
||||
@@ -682,6 +691,9 @@ async def test_unregister_service_removes_forwarder(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""``unregister_service`` drops the bridge-installed forwarder."""
|
||||
MockConfigEntry(
|
||||
domain="mirror_demo", title="Mirror", sandbox="built-in"
|
||||
).add_to_hass(hass)
|
||||
_bridge, main_channel, sandbox_channel = await _wire(hass)
|
||||
|
||||
try:
|
||||
|
||||
@@ -257,6 +257,9 @@ async def test_register_service_with_schema_validates_on_main(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Sandbox-mirrored service uses its reconstructed schema on main calls."""
|
||||
MockConfigEntry(domain="mock_svc", title="Mock", sandbox="built-in").add_to_hass(
|
||||
hass
|
||||
)
|
||||
main_channel, sandbox_channel = make_channel_pair(
|
||||
name_a="main-mock", name_b="sandbox-mock"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user