-
Notifications
You must be signed in to change notification settings - Fork 0
fix: restore critical security guardrails #675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -470,9 +470,55 @@ def is_host_excluded_by_no_proxy(hostname: str, no_proxy_value: str | None = Non | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| from gateway.config import Platform, PlatformConfig | ||||||||||||||||||||||||||||||||||||||
| from gateway.session import SessionSource, build_session_key | ||||||||||||||||||||||||||||||||||||||
| from hermes_constants import get_hermes_dir | ||||||||||||||||||||||||||||||||||||||
| from hermes_constants import get_hermes_dir, get_hermes_home | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _trusted_gateway_media_roots() -> tuple[Path, ...]: | ||||||||||||||||||||||||||||||||||||||
| """Return Hermes-managed directories allowed for model-emitted attachments.""" | ||||||||||||||||||||||||||||||||||||||
| home = get_hermes_home() | ||||||||||||||||||||||||||||||||||||||
| roots = [ | ||||||||||||||||||||||||||||||||||||||
| get_image_cache_dir(), | ||||||||||||||||||||||||||||||||||||||
| get_audio_cache_dir(), | ||||||||||||||||||||||||||||||||||||||
| get_video_cache_dir(), | ||||||||||||||||||||||||||||||||||||||
| get_document_cache_dir(), | ||||||||||||||||||||||||||||||||||||||
| get_hermes_dir("cache/screenshots", "browser_screenshots"), | ||||||||||||||||||||||||||||||||||||||
| get_hermes_dir("cache/vision", "temp_vision_images"), | ||||||||||||||||||||||||||||||||||||||
| home / "cache" / "images", | ||||||||||||||||||||||||||||||||||||||
| home / "cache" / "audio", | ||||||||||||||||||||||||||||||||||||||
| home / "cache" / "videos", | ||||||||||||||||||||||||||||||||||||||
| home / "cache" / "documents", | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
| resolved: list[Path] = [] | ||||||||||||||||||||||||||||||||||||||
| for root in roots: | ||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| resolved.append(Path(root).expanduser().resolve()) | ||||||||||||||||||||||||||||||||||||||
| except OSError: | ||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||
| return tuple(dict.fromkeys(resolved)) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+476
to
+497
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The _TRUSTED_MEDIA_ROOTS_CACHE: tuple[Path, ...] | None = None
def _trusted_gateway_media_roots() -> tuple[Path, ...]:
"""Return Hermes-managed directories allowed for model-emitted attachments."""
global _TRUSTED_MEDIA_ROOTS_CACHE
if _TRUSTED_MEDIA_ROOTS_CACHE is not None:
return _TRUSTED_MEDIA_ROOTS_CACHE
home = get_hermes_home()
roots = [
get_image_cache_dir(),
get_audio_cache_dir(),
get_video_cache_dir(),
get_document_cache_dir(),
get_hermes_dir("cache/screenshots", "browser_screenshots"),
get_hermes_dir("cache/vision", "temp_vision_images"),
home / "cache" / "images",
home / "cache" / "audio",
home / "cache" / "videos",
home / "cache" / "documents",
]
resolved: list[Path] = []
for root in roots:
try:
resolved.append(Path(root).expanduser().resolve())
except OSError:
continue
_TRUSTED_MEDIA_ROOTS_CACHE = tuple(dict.fromkeys(resolved))
return _TRUSTED_MEDIA_ROOTS_CACHE |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _is_trusted_gateway_media_path(path: str) -> bool: | ||||||||||||||||||||||||||||||||||||||
| """Return True when *path* is a regular file inside Hermes media caches.""" | ||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||
| candidate = Path(path).expanduser().resolve() | ||||||||||||||||||||||||||||||||||||||
| if not candidate.is_file(): | ||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||
| return any(candidate.is_relative_to(root) for root in _trusted_gateway_media_roots()) | ||||||||||||||||||||||||||||||||||||||
| except OSError: | ||||||||||||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+500
to
+508
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _filter_trusted_gateway_media_paths(paths): | ||||||||||||||||||||||||||||||||||||||
| """Drop model-emitted local paths outside Hermes-managed media caches.""" | ||||||||||||||||||||||||||||||||||||||
| trusted = [] | ||||||||||||||||||||||||||||||||||||||
| for item in paths: | ||||||||||||||||||||||||||||||||||||||
| path = item[0] if isinstance(item, tuple) else item | ||||||||||||||||||||||||||||||||||||||
| if _is_trusted_gateway_media_path(path): | ||||||||||||||||||||||||||||||||||||||
| trusted.append(item) | ||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||
| logger.warning("Blocked untrusted model-emitted media path: %s", path) | ||||||||||||||||||||||||||||||||||||||
| return trusted | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| GATEWAY_SECRET_CAPTURE_UNSUPPORTED_MESSAGE = ( | ||||||||||||||||||||||||||||||||||||||
| "Secure secret entry is not supported over messaging. " | ||||||||||||||||||||||||||||||||||||||
| "Load this skill in the local CLI to be prompted, or add the key to ~/.hermes/.env manually." | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -2982,6 +3028,8 @@ async def _stop_typing_task() -> None: | |||||||||||||||||||||||||||||||||||||
| # Extract MEDIA:<path> tags (from TTS tool) before other processing | ||||||||||||||||||||||||||||||||||||||
| media_files, response = self.extract_media(response) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| media_files = _filter_trusted_gateway_media_paths(media_files) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # Extract image URLs and send them as native platform attachments | ||||||||||||||||||||||||||||||||||||||
| images, text_content = self.extract_images(response) | ||||||||||||||||||||||||||||||||||||||
| # Strip any remaining internal directives from message body (fixes #1561) | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -2994,6 +3042,7 @@ async def _stop_typing_task() -> None: | |||||||||||||||||||||||||||||||||||||
| # Auto-detect bare local file paths for native media delivery | ||||||||||||||||||||||||||||||||||||||
| # (helps small models that don't use MEDIA: syntax) | ||||||||||||||||||||||||||||||||||||||
| local_files, text_content = self.extract_local_files(text_content) | ||||||||||||||||||||||||||||||||||||||
| local_files = _filter_trusted_gateway_media_paths(local_files) | ||||||||||||||||||||||||||||||||||||||
| if local_files: | ||||||||||||||||||||||||||||||||||||||
| logger.info("[%s] extract_local_files found %d file(s) in response", self.name, len(local_files)) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
cwdparameter can be passed as a string (e.g., from the session manager or ACP client payloads). Callingcwd.expanduser()directly on a string will raise anAttributeError, crashing the resource resolution. WrappingcwdinPath(cwd)ensures robust handling of bothPathandstrinputs.