From 26b2fc360f914985bba0ac94fcfe38ed5403fb05 Mon Sep 17 00:00:00 2001 From: buray Date: Tue, 17 Mar 2026 01:49:49 -0700 Subject: [PATCH] fix(skills): detect prompt injection patterns and block cache file reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two security layers to prevent prompt injection via skills hub cache files (#1558): 1. read_file: blocks direct reads of ~/.hermes/skills/.hub/ directory (index-cache, catalog files). The 3.5MB clawhub_catalog_v1.json was the original injection vector — untrusted skill descriptions in the catalog contained adversarial text that the model executed. 2. skill_view: warns when skills are loaded from outside the trusted ~/.hermes/skills/ directory, and detects common injection patterns in skill content ("ignore previous instructions", "", etc.). Cherry-picked from PR #1562 by ygd58. --- tools/file_tools.py | 21 +++++++++++++++++++++ tools/skills_tool.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/tools/file_tools.py b/tools/file_tools.py index ddcfcd567a..03470c3758 100644 --- a/tools/file_tools.py +++ b/tools/file_tools.py @@ -169,6 +169,27 @@ def clear_file_ops_cache(task_id: str = None): def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str = "default") -> str: """Read a file with pagination and line numbers.""" try: + # Security: block direct reads of internal Hermes cache/index files + # to prevent prompt injection via catalog or hub metadata files. + import pathlib as _pathlib + _resolved = _pathlib.Path(path).expanduser().resolve() + _hermes_home = _pathlib.Path("~/.hermes").expanduser().resolve() + _blocked_dirs = [ + _hermes_home / "skills" / ".hub" / "index-cache", + _hermes_home / "skills" / ".hub", + ] + for _blocked in _blocked_dirs: + try: + _resolved.relative_to(_blocked) + return json.dumps({ + "error": ( + f"Access denied: {path} is an internal Hermes cache file " + "and cannot be read directly to prevent prompt injection. " + "Use the skills_list or skill_view tools instead." + ) + }) + except ValueError: + pass file_ops = _get_file_ops(task_id) result = file_ops.read_file(path, offset, limit) if result.content: diff --git a/tools/skills_tool.py b/tools/skills_tool.py index bcde5d538b..771d7684f1 100644 --- a/tools/skills_tool.py +++ b/tools/skills_tool.py @@ -873,6 +873,37 @@ def skill_view(name: str, file_path: str = None, task_id: str = None) -> str: ensure_ascii=False, ) + # Security: warn if skill is loaded from outside the trusted skills directory + try: + skill_md.resolve().relative_to(SKILLS_DIR.resolve()) + _outside_skills_dir = False + except ValueError: + _outside_skills_dir = True + + # Security: detect common prompt injection patterns + _INJECTION_PATTERNS = [ + "ignore previous instructions", + "ignore all previous", + "you are now", + "disregard your", + "forget your instructions", + "new instructions:", + "system prompt:", + "", + "]]>", + ] + _content_lower = content.lower() + _injection_detected = any(p in _content_lower for p in _INJECTION_PATTERNS) + + if _outside_skills_dir or _injection_detected: + _warnings = [] + if _outside_skills_dir: + _warnings.append(f"skill file is outside the trusted skills directory (~/.hermes/skills/): {skill_md}") + if _injection_detected: + _warnings.append("skill content contains patterns that may indicate prompt injection") + import logging as _logging + _logging.getLogger(__name__).warning("Skill security warning for '%s': %s", name, "; ".join(_warnings)) + parsed_frontmatter: Dict[str, Any] = {} try: parsed_frontmatter, _ = _parse_frontmatter(content)