Back to skill

Security audit

Jlm Coffee

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Jerusalem coffee-shop lookup tool, but its cache handling creates a real local file-safety concern users should review before installing.

Install only if you are comfortable with a small Python CLI that fetches public Google Docs data and caches it locally. Avoid running it as an administrator or in shared multi-user environments until the cache is moved to a private per-user cache directory and terminal output is sanitized.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/jlm-coffee.py:18
Finding
Predictable Shared Temporary Cache Permits Cache Poisoning and Symlink-Based File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `scripts/jlm-coffee.py`, lines 18-19 and 91-113 **Vulnerability Type**: Insecure temporary file handling and symlink following **Risk Level**: Medium ### Vulnerable Code ```python CACHE_DIR = os.path.join(tempfile.gettempdir(), "jlm-coffee") CACHE_FILE = os.path.join(CACHE_DIR, "shops.json") ``` ```python def _read_cache(): """Return cached shops list or None if stale/missing.""" if _force_fresh: return None try: if not os.path.exists(CACHE_FILE): return None age = time.time() - os.path.getmtime(CACHE_FILE) if age > CACHE_TTL: return None with open(CACHE_FILE, "r", encoding="utf-8") as f: return json.load(f) except (json.JSONDecodeError, OSError): return None def _write_cache(shops): """Write shops list to cache file.""" try: os.makedirs(CACHE_DIR, exist_ok=True) with open(CACHE_FILE, "w", encoding="utf-8") as f: json.dump(shops, f, ensure_ascii=False) except OSError: pass # cache is best-effort ``` ### Technical Analysis The application stores cached data at the fixed, predictable path `/tmp/jlm-coffee/shops.json` on typical Linux systems. The system temporary directory is commonly shared among local users. Neither the cache directory nor the cache file is validated for ownership, file type, or symbolic-link status. The calls to `os.path.getmtime()` and `open()` follow symbolic links. The directory is also created without explicitly enforcing private permissions. This creates two related risks: 1. **Cache poisoning:** A local attacker can create the predictable cache file before the victim runs the application. If its modification time is sufficiently recent and its contents are valid JSON, `_read_cache()` trusts the attacker-controlled data. 2. **Symlink-based overwrite:** An attacker can place a symbolic link at the cache path. When `_write_cache()` ...[truncated 2342 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store the cache in a per-user cache directory, such as `$XDG_CACHE_HOME/jlm-coffee`, rather than a globally shared temporary location. 2. Create the cache directory with mode `0700` and verify that it is owned by the effective user. 3. Reject existing symbolic links and unexpected file types using `os.lstat()` or file-descriptor-based checks. 4. Open files with `O_NOFOLLOW` where supported so the operating system rejects symbolic links. 5. Write data to a securely created temporary file in the verified cache directory, set mode `0600`, flush and synchronize it, and atomically replace the cache with `os.replace()`. 6. Validate the owner, permissions, and regular-file status of an existing cache before reading it. 7. Do not silently reuse an attacker-controlled directory. If security checks fail, skip caching or terminate with a clear error. A hardened implementation should use a user-private directory and atomic replacement rather than writing directly to the final predictable path. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/jlm-coffee.py:205
Finding
Untrusted Remote Dataset Fields Are Printed Without Control-Character Sanitization<![CDATA[ ## Vulnerability Details **File Location**: `scripts/jlm-coffee.py`, lines 59-81 and 205-263 **Vulnerability Type**: Terminal control-sequence injection through untrusted remote content **Risk Level**: Low ### Vulnerable Code ```python def _fetch_data(): """Fetch the full JSON from the public Google Doc export.""" try: resp = urllib.request.urlopen(DATA_URL, timeout=15) raw = resp.read() text = raw.decode("utf-8-sig") # Google Doc export includes BOM data = json.loads(text) return data except urllib.error.HTTPError as e: print(f"Error: HTTP {e.code} fetching data from Google Docs", file=sys.stderr) sys.exit(1) except urllib.error.URLError as e: print(f"Error: Could not connect - {e.reason}", file=sys.stderr) sys.exit(1) except json.JSONDecodeError as e: print(f"Error: Invalid JSON in data source - {e}", file=sys.stderr) sys.exit(1) ``` ```python def format_shop_detail(shop): lines = [] name = shop.get("name", "?") lines.append(f"{brown('===')} {bold(name)} {brown('===')}") lines.append(f" {dim('ID:')} {shop['id']}") rating = shop.get("avgRating", 0) reviews = shop.get("totalReviews", 0) lines.append(f" {dim('Rating:')} {format_stars(rating)} ({reviews} reviews)") desc = shop.get("description", "") if desc: lines.append(f" {dim('Description:')} {desc}") address = shop.get("address", "") if address: lines.append(f" {dim('Address:')} {address}") amenities = shop.get("amenities", []) if amenities and isinstance(amenities, list): tags = ", ".join(AMENITY_LABELS.get(a, a) for a in amenities) lines.append(f" {dim('Amenities:')} {tags}") loc = shop.get("location") if loc and isinstance(loc, dict): lat = loc.get("lat", "?") lng = loc.get("lng", "?") lines.append(f" {dim('Location:')} {lat}, {lng}") lines.append(f" {di ...[truncated 4147 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Treat every textual value in the fetched dataset as untrusted. 2. Before human-readable terminal output, remove C0 and C1 control characters except explicitly permitted whitespace such as newline and tab. 3. Remove or visibly encode the ESC character and ANSI/OSC control sequences, including sequences terminated by BEL or string terminators. 4. Normalize or escape carriage returns, backspaces, bidirectional control characters, and other characters that can alter visual presentation. 5. Apply strict schema validation to the downloaded JSON, including expected types, maximum string lengths, numeric ranges, URL schemes, and required identifiers. 6. Keep JSON mode as machine-readable output, but ensure downstream Agents are informed that all dataset strings are externally supplied data and must not be interpreted as instructions. 7. Consider integrity or provenance controls for the remote dataset and restrict document modification permissions. 8. Apply the same validation after reading cached data, because local cache poisoning can bypass protections applied only during network retrieval. ]]>
Vulnerability Patterns
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (2)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill advertises executable behavior that can read environment variables, access the filesystem, write local cache files, and make network requests, but it does not declare any explicit tool scope or permission boundaries. This creates a least-privilege and reviewability problem: a host may grant broader capabilities than users or operators expect, and network/file access can expose local data or enable unintended external communication if the implementation is modified or abused.

Natural-Language Policy Violations

Low
Confidence
82% confidence
Finding
This code defines English-only day names for displayed opening hours, which can impose a specific language in user-facing output. The file does not provide any user opt-in, locale selection, or justification for restricting output language.

Static analysis

No suspicious patterns detected.