Back to skill

Security audit

Website Monitor

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its website-monitoring purpose, but it fetches unrestricted URLs and stores full page text in a broadly configurable state directory despite documenting hash-only storage.

Install only if you are comfortable with the agent fetching URLs from your machine and saving fetched page text locally. Use trusted URL lists, avoid monitoring private/internal endpoints unless intended, choose a private state directory you control instead of shared /tmp, clean old state files when no longer needed, and install dependencies in a dedicated environment with pinned versions where possible.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/monitor.py:24
Finding
Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/monitor.py:24-37` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Complete Code Snippet ```python def fetch_page(url, timeout=15): """Fetch a URL and return (status_code, text, response_time_ms).""" start = time.time() try: resp = requests.get(url, headers=HEADERS, timeout=timeout, allow_redirects=True) elapsed = int((time.time() - start) * 1000) # Extract text content (strip HTML roughly) text = re.sub(r"<script[^>]*>.*?</script>", "", resp.text, flags=re.DOTALL) text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL) text = re.sub(r"<[^>]+>", " ", text) text = re.sub(r"\s+", " ", text).strip() return resp.status_code, text, elapsed except requests.exceptions.Timeout: elapsed = int((time.time() - start) * 1000) return 0, "", elapsed ``` All command modes ultimately pass caller-controlled URLs to this function. The relevant command arguments are defined at `scripts/monitor.py:153-166`: ```python p_check.add_argument("url") p_watch = sub.add_parser("watch", help="Detect changes vs last snapshot") p_watch.add_argument("url") p_watch.add_argument("--state-dir", default="/tmp/monitor-state") p_match = sub.add_parser("match", help="Check for content pattern") p_match.add_argument("url") p_match.add_argument("--pattern", required=True) p_batch = sub.add_parser("batch", help="Batch check from file") p_batch.add_argument("file") p_batch.add_argument("--state-dir", default="/tmp/monitor-state") ``` ### Technical Analysis The monitor makes outbound requests to user-controlled URLs without validating the URL scheme, hostname, port, or resolved IP address. It also enables automatic redirects through `allow_redirects=True` without validating each redirect destination. An attacker who can influence a direct command argument or an entry in a batch file can caus ...[truncated 1908 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Accept only explicitly supported schemes, preferably `https` and, where necessary, `http`. 2. Reject URLs containing embedded credentials or malformed authority components. 3. Resolve the destination hostname before connecting and reject every resolved address belonging to loopback, private, link-local, multicast, unspecified, reserved, or otherwise non-public ranges. 4. Disable automatic redirects or process redirects manually and repeat the complete scheme, hostname, and resolved-IP validation for every destination. 5. Explicitly block known cloud metadata endpoints and link-local metadata ranges. 6. Consider enforcing a domain allowlist when the intended monitoring targets are known. 7. Account for DNS rebinding by ensuring that the validated IP is the address actually used for the connection. 8. Apply outbound firewall or proxy controls so the monitoring process cannot access sensitive internal networks. 9. Add tests covering IPv4, IPv6, alternate address representations, redirects, DNS rebinding scenarios, and public hostnames resolving to private addresses. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/monitor.py:62
Finding
Caller-Controlled State Directory Permits Unsafe Filesystem Writes<![CDATA[ ## Vulnerability Details **File Location**: `scripts/monitor.py:62-108` **Vulnerability Type**: Unrestricted file write location and unsafe state-file handling **Risk Level**: Medium ### Complete Code Snippet ```python def cmd_watch(args): """Monitor for changes vs last snapshot.""" state_dir = args.state_dir or "/tmp/monitor-state" os.makedirs(state_dir, exist_ok=True) state_file = os.path.join(state_dir, f"{url_hash(args.url)}.txt") status, text, ms = fetch_page(args.url) if status != 200: print(f"❌ DOWN — {args.url} (HTTP {status}, {ms}ms)") return 2 current_hash = hashlib.md5(text.encode()).hexdigest() if os.path.exists(state_file): with open(state_file) as f: old_text = f.read() old_hash = hashlib.md5(old_text.encode()).hexdigest() if current_hash == old_hash: print(f"✅ UNCHANGED — {args.url} ({ms}ms)") # Update state file with open(state_file, "w") as f: f.write(text) return 0 else: print(f"🔄 CHANGED — {args.url} ({ms}ms)") # Show diff summary old_lines = old_text[:2000].splitlines() new_lines = text[:2000].splitlines() diff = list(difflib.unified_diff(old_lines, new_lines, lineterm="", n=1)) for line in diff[:20]: print(f" {line}") if len(diff) > 20: print(f" ... ({len(diff) - 20} more diff lines)") # Update state with open(state_file, "w") as f: f.write(text) return 1 else: print(f"📝 FIRST CHECK — {args.url} ({ms}ms, {len(text)} chars)") with open(state_file, "w") as f: f.write(text) return 0 ``` The same pattern is present in batch mode at `scripts/monitor.py:126-147`: ```python state_dir = args.state_dir or "/tmp/monitor-state" results = {"up": 0, "down": 0, "changed": 0} ...[truncated 3102 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store state only beneath a fixed, application-owned directory rather than accepting an unrestricted filesystem path. 2. If configurable storage is required, canonicalize the requested path and verify that it remains beneath an approved root. 3. Create the state directory with restrictive permissions and verify that it is owned by the expected user and is not a symbolic link. 4. Open state files using symlink-resistant operating-system flags such as `O_NOFOLLOW` where supported. 5. Verify with `lstat` or equivalent checks that existing state entries are regular files owned by the expected account. 6. Write updates to a securely created temporary file in the same directory, flush them as required, and atomically replace the destination. 7. Use restrictive file permissions such as `0600`. 8. Avoid globally shared temporary directories for persistent state, or create a private per-user subdirectory using secure temporary-directory facilities. 9. Document the trust requirements for `--state-dir` and reject paths that do not satisfy ownership and permission checks. ]]>

T08 · Insecure Dependencies

Note
Location
SKILL.md:60
Finding
Unpinned Third-Party Dependency Installation<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:60-64` **Vulnerability Type**: Unpinned dependency and non-reproducible installation **Risk Level**: Low ### Complete Code Snippet ```markdown ## Dependencies ```bash pip3 install requests ``` ``` The runtime error in `scripts/monitor.py:12-16` repeats the same unpinned installation instruction: ```python try: import requests except ImportError: print("Error: requests required. Install with: pip3 install requests") sys.exit(1) ``` ### Technical Analysis The installation instruction retrieves the current version of `requests` and its transitive dependencies without a version constraint, lock file, or integrity hashes. Consequently, installations performed at different times may resolve to different artifacts. No malicious or typosquatted package name was identified: `requests` is the expected dependency. The risk arises from the absence of reproducible dependency controls. If a future release or transitive package is compromised, unexpectedly incompatible, or obtained from an untrusted configured package index, users following the documented command may install and execute it. ### Attack Path 1. A user follows the documented dependency installation instruction. 2. `pip` queries its configured package index and resolves the newest acceptable version of `requests` and its transitive dependencies. 3. No lock file or hashes constrain the selected versions or artifacts. 4. A compromised, substituted, or unexpectedly changed package is downloaded and installed. 5. Package installation or subsequent import executes code in the user's environment. This path depends on compromise or substitution in the package supply chain or on unsafe local package-index configuration; the project itself does not provide a malicious dependency. ### Impact Assessment A compromised dependency can execute code with the privileges of the user performing installation or running the monitor. This could a ...[truncated 309 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Declare the dependency in a version-controlled requirements or lock file. 2. Pin an audited version of `requests` and all transitive dependencies. 3. Include package hashes and install with hash verification, such as `pip install --require-hashes -r requirements.txt`. 4. Use a dedicated virtual environment rather than modifying a shared Python installation. 5. Install only from explicitly trusted package indexes and disable unintended extra indexes. 6. Add automated dependency vulnerability scanning and a controlled update process. 7. Update `SKILL.md` and the runtime error message to reference the locked requirements file instead of recommending an unconstrained installation. ]]>
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
91% confidence
Finding
The skill advertises and documents capabilities that perform network access and persistent file writes, but it does not declare any tool scope or permission boundary in the skill metadata. In an agent ecosystem, this creates a mismatch between what the skill can cause an agent to do and what reviewers or policy layers can easily see, increasing the chance of unintended outbound requests or disk writes being invoked without explicit user awareness.

Missing User Warnings

Low
Confidence
83% confidence
Finding
The documentation tells users to use a persistent state directory for monitoring but does not clearly warn that the skill stores page-derived data on disk over time. This can lead to unexpected retention of potentially sensitive webpage content, metadata, or diffs in shared or insecure temporary locations such as /tmp, especially on multi-user systems.

Static analysis

No suspicious patterns detected.