From 1f1d346ceda2b133f9bb4f5758e31b6190a211fe Mon Sep 17 00:00:00 2001 From: Erosika Date: Tue, 30 Jun 2026 20:38:46 +0000 Subject: [PATCH] fix(profile): resolve WhatsApp media-path cache roots per-call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inbound-media validator _is_allowed_bridge_path() checked against IMAGE_CACHE_DIR / AUDIO_CACHE_DIR / VIDEO_CACHE_DIR / DOCUMENT_CACHE_DIR value-imported at module load. After the base.py cache-dir getters became per-call resolvers, the bridge writes media into the active profile's cache while the validator still matched the frozen launch-profile constants — so media was rejected under a profile override (multi-profile gateway). Resolve the cache roots per-call via the get_*_cache_dir() getters and drop the now-unused frozen value-imports. Caught by automated review on #55867. --- plugins/platforms/whatsapp/adapter.py | 22 ++++-- .../test_whatsapp_media_path_profile.py | 67 +++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 tests/gateway/test_whatsapp_media_path_profile.py diff --git a/plugins/platforms/whatsapp/adapter.py b/plugins/platforms/whatsapp/adapter.py index 69e5df0d57d..c3c996b7340 100644 --- a/plugins/platforms/whatsapp/adapter.py +++ b/plugins/platforms/whatsapp/adapter.py @@ -272,10 +272,6 @@ from gateway.platforms.base import ( SUPPORTED_DOCUMENT_TYPES, cache_image_from_url, cache_audio_from_url, - IMAGE_CACHE_DIR, - AUDIO_CACHE_DIR, - VIDEO_CACHE_DIR, - DOCUMENT_CACHE_DIR, ) from utils import env_int @@ -296,7 +292,23 @@ def _is_allowed_bridge_path(url: str) -> bool: resolved = Path(url).resolve() except (OSError, ValueError): return False - for root in (IMAGE_CACHE_DIR, AUDIO_CACHE_DIR, VIDEO_CACHE_DIR, DOCUMENT_CACHE_DIR): + # Resolve the cache roots per-call via the getters (not the import-time + # constants) so this validator follows the active profile override; under a + # profile override the inbound bridge writes media into that profile's + # cache, which the frozen constants would not match. + from gateway.platforms.base import ( + get_audio_cache_dir, + get_document_cache_dir, + get_image_cache_dir, + get_video_cache_dir, + ) + + for root in ( + get_image_cache_dir(), + get_audio_cache_dir(), + get_video_cache_dir(), + get_document_cache_dir(), + ): try: if resolved.is_relative_to(Path(root).resolve()): return True diff --git a/tests/gateway/test_whatsapp_media_path_profile.py b/tests/gateway/test_whatsapp_media_path_profile.py new file mode 100644 index 00000000000..34243bcb4c4 --- /dev/null +++ b/tests/gateway/test_whatsapp_media_path_profile.py @@ -0,0 +1,67 @@ +"""Regression: the WhatsApp inbound-media path validator follows the active +profile override. + +The bridge writes inbound media into the active profile's cache (resolved via +get_image_cache_dir() etc.). _is_allowed_bridge_path() validates those paths. +It previously checked against IMAGE_CACHE_DIR / AUDIO_CACHE_DIR module +constants value-imported at import time, so under a profile override the +validator rejected media the bridge had legitimately written into the override +profile's cache. The validator must resolve the cache roots per-call. +""" +from pathlib import Path + +from hermes_constants import reset_hermes_home_override, set_hermes_home_override + + +def _make_profile(root: Path) -> Path: + for sub in ("cache/images", "cache/audio", "cache/videos", "cache/documents"): + (root / sub).mkdir(parents=True, exist_ok=True) + return root + + +def test_validator_accepts_active_profile_media(tmp_path): + from plugins.platforms.whatsapp.adapter import _is_allowed_bridge_path + + prof = _make_profile(tmp_path / "profB") + media = prof / "cache" / "images" / "img_abc.jpg" + media.write_bytes(b"\xff\xd8\xff\x00") + + token = set_hermes_home_override(str(prof)) + try: + assert _is_allowed_bridge_path(str(media)) is True + finally: + reset_hermes_home_override(token) + + +def test_validator_follows_override_switch(tmp_path): + """A path under profile A is rejected while the override is profile B.""" + from plugins.platforms.whatsapp.adapter import _is_allowed_bridge_path + + prof_a = _make_profile(tmp_path / "profA") + prof_b = _make_profile(tmp_path / "profB") + a_media = prof_a / "cache" / "images" / "img_a.jpg" + a_media.write_bytes(b"\xff\xd8\xff\x00") + + # Under B, B's own media validates... + b_media = prof_b / "cache" / "images" / "img_b.jpg" + b_media.write_bytes(b"\xff\xd8\xff\x00") + token = set_hermes_home_override(str(prof_b)) + try: + assert _is_allowed_bridge_path(str(b_media)) is True + # ...and a path from a *different* profile is not in B's cache roots. + assert _is_allowed_bridge_path(str(a_media)) is False + finally: + reset_hermes_home_override(token) + + +def test_validator_rejects_non_cache_path(tmp_path): + from plugins.platforms.whatsapp.adapter import _is_allowed_bridge_path + + prof = _make_profile(tmp_path / "profB") + outside = tmp_path / "etc_passwd" + outside.write_text("root:x:0:0") + token = set_hermes_home_override(str(prof)) + try: + assert _is_allowed_bridge_path(str(outside)) is False + finally: + reset_hermes_home_override(token)