diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index b1048420ea91..4a8eb770b9f1 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -603,6 +603,9 @@ def _validate_signature( self, request: "web.Request", body: bytes, secret: str ) -> bool: """Validate webhook signature (GitHub, GitLab, generic HMAC-SHA256).""" + # Reject unresolved ${VAR} env-template placeholders — these would + # otherwise be HMAC'd verbatim, accepting any caller who guesses the + # literal ``${WEBHOOK_SECRET}`` string as the secret. if _looks_unresolved_secret(secret): logger.warning( "[webhook] Unresolved placeholder secret configured (e.g. ${WEBHOOK_SECRET}) — rejecting" diff --git a/gateway/run.py b/gateway/run.py index d40ae53c9899..6c17bf6bb967 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -5373,7 +5373,7 @@ def _is_user_authorized(self, source: SessionSource) -> bool: user_id = source.user_id if not user_id: return False - team_id = (source.guild_id or "").strip() if source.platform == Platform.SLACK else "" + team_id = str(source.guild_id or "").strip() if source.platform == Platform.SLACK else "" platform_env_map = { Platform.TELEGRAM: "TELEGRAM_ALLOWED_USERS", diff --git a/gateway/session_context.py b/gateway/session_context.py index d4e4adb4dc12..4539d8fbf4fe 100644 --- a/gateway/session_context.py +++ b/gateway/session_context.py @@ -36,6 +36,7 @@ platform = get_session_env("HERMES_SESSION_PLATFORM", "") """ +import os from contextvars import ContextVar from typing import Any @@ -151,8 +152,6 @@ def get_session_env(name: str, default: str = "") -> str: don't use ``set_session_vars`` at all). 3. *default* """ - import os - var = _VAR_MAP.get(name) if var is not None: value = var.get() @@ -180,8 +179,6 @@ def get_terminal_cwd(default=None): take precedence so concurrent gateway/cron sessions cannot clobber each other. """ - import os - value = _TERMINAL_CWD.get() if value is not _UNSET: return value diff --git a/tools/browser_tool.py b/tools/browser_tool.py index 79fce9fa6167..652456b5c7a6 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -126,12 +126,10 @@ def _browser_url_security_block(url: str, *, auto_local_this_nav: bool = False) ), } - if not _is_local_backend() and _is_always_blocked_url(url): + if _is_always_blocked_url(url): return {"success": False, "error": "Blocked: URL targets a cloud metadata endpoint"} if _is_camofox_mode(): - if _is_always_blocked_url(url): - return {"success": False, "error": "Blocked: URL targets a cloud metadata endpoint"} if not _is_safe_url(url): return {"success": False, "error": "Blocked: URL targets a private or internal address"} @@ -2298,23 +2296,19 @@ def browser_navigate(url: str, task_id: Optional[str] = None) -> str: # 169.254.169.254 / metadata.google.internal / ECS task metadata # via a browser, and routing those to a local Chromium sidecar # on an EC2/GCP/Azure host exfiltrates IAM credentials (#16234). - if not _is_local_backend() and _is_always_blocked_url(url): + # Local backends are NOT exempt — an agent on EC2 with a local + # Chromium and allow_private_urls=true could otherwise hit IMDS. + if _is_always_blocked_url(url): return json.dumps({ "success": False, "error": "Blocked: URL targets a cloud metadata endpoint", }) - if _is_camofox_mode(): - if _is_always_blocked_url(url): - return json.dumps({ - "success": False, - "error": "Blocked: URL targets a cloud metadata endpoint", - }) - if not _is_safe_url(url): - return json.dumps({ - "success": False, - "error": "Blocked: URL targets a private or internal address", - }) + if _is_camofox_mode() and not _is_safe_url(url): + return json.dumps({ + "success": False, + "error": "Blocked: URL targets a private or internal address", + }) if ( not auto_local_this_nav @@ -2378,11 +2372,10 @@ def browser_navigate(url: str, task_id: Optional[str] = None) -> str: # and for the hybrid local sidecar (we're already on a local browser # hitting a private URL by design). # Always-blocked floor (cloud metadata / IMDS) is enforced even - # when auto_local_this_nav is true — see pre-nav check for - # rationale (#16234). + # when auto_local_this_nav is true and for local backends — see + # pre-nav check for rationale (#16234). if ( - not _is_local_backend() - and final_url + final_url and final_url != url and _is_always_blocked_url(final_url) ): diff --git a/tools/web_tools.py b/tools/web_tools.py index 7933ccbfe196..84fda6bf3b02 100644 --- a/tools/web_tools.py +++ b/tools/web_tools.py @@ -224,8 +224,9 @@ def _ddgs_package_available() -> bool: return ddgs_package_available() +# Backward-compat alias for older tests / external callers that imported +# the previous name. New code should call ``_ddgs_package_available``. def _ddgs_package_importable() -> bool: - """Backward-compat alias for :func:`_ddgs_package_available`.""" return _ddgs_package_available() # ─── Firecrawl Client ────────────────────────────────────────────────────────