Back to skill

Security audit

Ecommerce Price Watcher

Security checks for vulnerabilities and agentic risk

Overview

This price-watcher is mostly coherent, but it can make unrestricted web requests from the user's machine, including to internal or private network targets via supplied URLs or redirects.

Install only if you are comfortable with this skill making outbound requests from your machine and storing watched product data locally. Avoid adding internal, localhost, private-network, or sensitive URLs, and do not treat --trusted-only as a strict security boundary until redirect and private-address validation are added.

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

Error
Location
scripts/price_watch.py:121
Finding
Server-Side Request Forgery Through Unrestricted Product URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/price_watch.py`, lines 121–143; related input and execution flow at lines 206–207 and 275–277 **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```python def validate_url(url: str) -> None: p = urlparse(url) if p.scheme not in {"http", "https"}: raise ValueError("Only http/https URLs are allowed") if not p.netloc: raise ValueError("Invalid URL") def fetch_html(url: str) -> str: validate_url(url) req = Request(url, headers={"User-Agent": UA}) with urlopen(req, timeout=TIMEOUT) as r: content_type = (r.headers.get("Content-Type") or "").lower() if "text/html" not in content_type and "application/xhtml+xml" not in content_type: raise ValueError(f"Unsupported content type: {content_type}") raw = r.read(MAX_BYTES + 1) if len(raw) > MAX_BYTES: raise ValueError("Response too large") return raw.decode("utf-8", errors="replace") ``` The direct URL is accepted and persisted through this flow: ```python def cmd_add(args: argparse.Namespace) -> int: data = load_store() result = add_watch_item(data, args.url, args.target_price, args.currency) save_store(data) print(json.dumps({"ok": True, "item": result, "store": str(STORE_PATH)})) return 0 ``` Stored URLs are subsequently fetched through this flow: ```python def check_one(item: Dict[str, Any]) -> Dict[str, Any]: checked_at = now_iso() try: title, price, debug = parse_product(item["url"]) except Exception as e: return {"id": item["id"], "ok": False, "error": str(e), "checkedAt": checked_at} ``` ### Technical Analysis The URL validation only confirms that the scheme is `http` or `https` and that a network location exists. It does not reject: - Loopback addresses such as `127.0.0.1` or `::1` - Private network ranges - Link-local addresses - Cloud instance ...[truncated 2561 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate URLs both when they are added and immediately before every request. 2. Resolve the destination hostname and reject every address in loopback, private, link-local, multicast, reserved, or unspecified IPv4 and IPv6 ranges. 3. Disable automatic redirect handling or implement a custom redirect handler that validates every redirect target before following it. 4. Impose a strict redirect limit and reject HTTPS-to-HTTP downgrade redirects. 5. Mitigate DNS rebinding by connecting only to a validated resolved address while preserving correct TLS hostname verification and the intended HTTP `Host` value. 6. Consider enforcing an explicit allowlist of approved ecommerce domains for all product fetches. 7. Reject URLs containing unexpected credentials, malformed hostnames, or ambiguous address representations. 8. Apply outbound firewall or proxy rules so the process cannot reach internal networks or metadata services. 9. Minimize response data returned in errors and debugging fields to reduce information disclosure. 10. Add automated tests covering IPv4, IPv6, encoded IP representations, DNS rebinding scenarios, redirect chains, localhost, private ranges, and cloud metadata addresses. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/price_watch.py:178
Finding
Trusted-Domain Restriction Can Be Bypassed Through Redirects<![CDATA[ ## Vulnerability Details **File Location**: `scripts/price_watch.py`, lines 178–180 and 199–201; redirect-following request at lines 137–143 **Vulnerability Type**: Trusted-domain allowlist bypass **Risk Level**: Medium ### Vulnerable Code The trusted-domain check is applied only to the initially discovered URL: ```python def domain_matches(url: str, allowlist: List[str]) -> bool: host = urlparse(url).netloc.lower() return any(host == d or host.endswith("." + d) for d in allowlist) ``` ```python for m in DDG_LINK_RE.finditer(html): href = html_lib.unescape(m.group(1)) href = parse_ddg_redirect(href) if not href.startswith("http"): continue try: validate_url(href) except ValueError: continue if trusted_only and not domain_matches(href, TRUSTED_DOMAINS): continue if href in seen: continue seen.add(href) urls.append(href) if len(urls) >= max_results: break ``` The later fetch automatically follows redirects without applying the trusted-domain policy to each destination: ```python def fetch_html(url: str) -> str: validate_url(url) req = Request(url, headers={"User-Agent": UA}) with urlopen(req, timeout=TIMEOUT) as r: content_type = (r.headers.get("Content-Type") or "").lower() if "text/html" not in content_type and "application/xhtml+xml" not in content_type: raise ValueError(f"Unsupported content type: {content_type}") raw = r.read(MAX_BYTES + 1) if len(raw) > MAX_BYTES: raise ValueError("Response too large") return raw.decode("utf-8", errors="replace") ``` ### Technical Analysis The `--trusted-only` option checks the hostname of a search result before adding it. That policy is not stored as an enforceable security property on the watcher and is not re-applied when the product page is fetched. Because `urlopen()` follows redirects automatically, a URL that initially belongs t ...[truncated 1845 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Persist the `trusted_only` policy on each watcher so it remains enforceable during every later check. 2. Disable automatic redirects and process redirect responses explicitly. 3. Validate every redirect hop against the trusted-domain allowlist before sending the next request. 4. Reject cross-domain redirects in trusted-only mode unless the destination is separately allowlisted. 5. Combine domain validation with IP-address validation so an allowlisted hostname cannot resolve to a private or otherwise restricted destination. 6. Normalize hostnames before comparison, including lowercasing, removing a trailing dot, handling internationalized names safely, and separating the hostname from any port. 7. Limit the number of redirects and detect redirect loops. 8. Include the final validated URL in results for auditability without exposing sensitive response content. 9. Add tests for same-domain redirects, cross-domain redirects, redirect chains, open redirects, internationalized hostnames, explicit ports, and redirects to private addresses. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (3)

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill documentation describes code that performs network access, reads and writes watcher state, and discovers URLs, but it does not declare any explicit tool scope or permissions boundary. In an agent environment, this increases the chance the skill is granted broader-than-necessary capabilities, making unintended outbound requests, local file access, or persistence harder to audit and constrain.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The script performs outbound HTTP requests to arbitrary user-supplied URLs and to URLs discovered from a search engine, but it does not provide any explicit notice or consent boundary about network access. In an agent setting, this can expose the user's IP, user agent, query terms, and potentially internal-only URLs if a user supplies them, making the behavior security-relevant even though it is core to the skill's purpose.

Missing User Warnings

Low
Confidence
88% confidence
Finding
The skill silently creates and maintains a persistent local JSON store containing watched URLs, titles, prices, timestamps, queries, and history. In multi-user or sensitive environments, retaining this data without clear notice can leak shopping interests or monitored links beyond the immediate session and increase privacy risk.

Static analysis

No suspicious patterns detected.