Back to skill

Security audit

Safe Smart Web Fetch

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent web-fetch helper, but its safety checks are not strong enough for sensitive URLs or private-network fetching.

Review before installing in environments with access to private networks, cloud metadata, internal tools, or sensitive links. Use it only with URLs you are comfortable fetching from the agent host, and assume ordinary-looking URLs may be sent to Jina Reader, markdown.new, or defuddle.md unless the script classifies them as sensitive.

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/fetch.py:53
Finding
Credential-Bearing and Sensitive URLs Can Be Disclosed to Third-Party Services<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch.py:53-67, 97-103` **Vulnerability Type**: Incomplete sensitive-data detection and external URL disclosure **Risk Level**: High ### Complete Code Snippet ```python def has_sensitive_query(url: str) -> bool: parsed = urllib.parse.urlparse(url) params = urllib.parse.parse_qsl(parsed.query, keep_blank_values=True) frag = parsed.fragment.lower() for k, v in params: lk = k.lower() lv = (v or '').lower() if lk in SENSITIVE_KEYS: return True if any(s in lk for s in SENSITIVE_KEYS): return True if len(v) > 20 and any(x in lk for x in ['token', 'code', 'sig', 'key', 'auth']): return True if 'bearer' in lv: return True if any(x in frag for x in ['access_token', 'token=', 'session=', 'code=']): return True return False ``` ```python def clean_service_urls(original_url: str): stripped = original_url.replace('https://', '').replace('http://', '') return [ ('jina', f'https://r.jina.ai/http://{stripped}'), ('markdown-new', f'https://markdown.new/{original_url}'), ('defuddle', f'https://defuddle.md/{original_url}'), ] ``` ### Technical Analysis The Skill deliberately sends URLs classified as public to Jina Reader, markdown.new, and defuddle.md. This is consistent with its declared content-cleaning functionality, but the classification does not adequately prevent sensitive information from being transmitted. The sensitive-data check only recognizes a fixed set of query-parameter names and several fragment patterns. It does not reject URL user information such as `https://username:password@example.com/`, and it cannot reliably identify secrets stored under unrecognized parameter names or fragment formats. After classification, `clean_service_urls()` embeds the entire original URL into requests to external services. This includes its query strin ...[truncated 1865 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject URLs containing a nonempty `username` or `password` component before any network request. 2. Remove URL fragments before submitting a URL to a third-party service because fragments are unnecessary for normal server-side retrieval. 3. Strip all query parameters by default before third-party processing, or use a narrowly defined allowlist of parameters known to be public. 4. If query preservation is required, require explicit user confirmation after clearly identifying the third-party recipient and the exact sanitized URL. 5. Normalize and validate the URL once, then construct third-party requests from its parsed components rather than using string replacement. 6. Preserve the original HTTPS scheme when instructing a cleaning service to retrieve the target. 7. Avoid sending a URL sequentially to multiple providers unless necessary. Prefer a user-selected provider or an explicitly configured trusted service. 8. Document that third-party processing discloses the destination URL to external providers. 9. Add tests covering URL user information, encoded parameter names, mixed case, duplicate parameters, fragments, signed URLs, and application-specific secret names. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch.py:27
Finding
Redirect and DNS Validation Gaps Permit Server-Side Request Forgery into Private Networks<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch.py:27-48, 70-94, 107-128` **Vulnerability Type**: Server-side request forgery through redirects, DNS resolution, or DNS rebinding **Risk Level**: High ### Complete Code Snippet ```python def is_private_host(host: str) -> bool: if not host: return True h = host.lower().strip('[]') if h in {'localhost'} or h.endswith('.local'): return True try: ip = ipaddress.ip_address(h) return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved except ValueError: pass if re_private_name(h): return True return False def re_private_name(host: str) -> bool: # No public suffix; conservatively treat short LAN-style hostnames as private return '.' not in host or host.endswith('.lan') or host.endswith('.home') or host.endswith('.internal') ``` ```python def classify_url(url: str) -> Tuple[bool, str]: parsed = urllib.parse.urlparse(url) if parsed.scheme not in {'http', 'https'}: return False, 'non-http-url' host = parsed.hostname or '' if is_private_host(host): return False, 'private-or-local-host' if looks_private_link(url): return False, 'sensitive-or-private-link' return True, '' def fetch_url(url: str, timeout: int = TIMEOUT) -> Dict: req = urllib.request.Request(url, headers={'User-Agent': USER_AGENT}) with urllib.request.urlopen(req, timeout=timeout, context=ssl_context) as response: content = response.read().decode('utf-8', errors='ignore') return {'success': response.status == 200, 'content': content, 'status': response.status} ``` ```python def get_content(url: str) -> Dict: allow_third_party, blocked_reason = classify_url(url) if allow_third_party: for source, clean_url in clean_service_urls(url): try: result = fetch_url(clean_url) if result['success'] and len(result[' ...[truncated 3533 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve each destination hostname before connecting and reject every address that is not globally routable. 2. Explicitly reject loopback, private, link-local, multicast, unspecified, reserved, carrier-grade NAT, and special-use address ranges for both IPv4 and IPv6. 3. Disable automatic redirects. Process each `Location` header manually and repeat scheme, user-information, hostname, port, and resolved-address validation for every hop. 4. Set a small maximum redirect count and reject cross-scheme redirects or redirects introducing credentials. 5. Pin the validated IP address for the actual connection while preserving the intended hostname for TLS certificate verification and the HTTP `Host` header. This reduces DNS rebinding and time-of-check/time-of-use risk. 6. Apply the same validation to direct fallback requests and third-party service endpoints. 7. Consider prohibiting private-network fetching entirely by default. If it is a required feature, place it behind an explicit opt-in and a destination allowlist. 8. Restrict outbound connectivity at the operating-system or container level so the Skill cannot reach cloud metadata endpoints, localhost administration ports, or unrelated private networks. 9. Add automated tests for public-to-private redirects, multi-hop redirects, hostnames resolving to private addresses, IPv6 local addresses, alternate IP representations, and DNS rebinding scenarios. ]]>
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 (4)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill performs network-capable actions but does not declare an explicit tool scope such as permissions or allowed-tools. In an agent environment, this creates ambiguity about what the skill is allowed to access and weakens policy enforcement, which can lead to unintended outbound requests or misuse of broader-than-expected tooling.

Natural-Language Policy Violations

Medium
Confidence
86% confidence
Finding
The manifest description and primary usage text are presented in Chinese only, which can impose a specific language on users without explicit opt-in. Under the policy, language constraints should either offer user choice or be clearly documented as justified for a specific locale.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
For URLs classified as public, the code automatically forwards the target to third-party services (r.jina.ai, markdown.new, defuddle.md) before attempting a direct fetch. This can disclose user-supplied URLs, query strings, and potentially sensitive page content to external providers without explicit consent, and the heuristic classification can miss sensitive-but-public links or uncommon secret-bearing parameters.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
Natural-language policy violations apply to all file types, including comments. The inline comment at L44 is written only in Chinese, which imposes a specific language choice without any indication that the skill is region-specific or that users/developers can opt into that locale.

Static analysis

No suspicious patterns detected.