Back to skill

Security audit

SEO Companion

Security checks for vulnerabilities and agentic risk

Overview

This SEO skill is mostly transparent, but its URL audit script can be tricked into fetching internal network pages through redirects, so it needs review before use.

Use this skill only for public sites and preferably in an environment without access to sensitive internal services. Before broad installation, fix the audit script to disable automatic redirects or validate every redirect destination, and keep package installation user-approved.

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 (1)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/audit_page.py:138
Finding
Server-Side Request Forgery Through Unvalidated Redirect Destinations<![CDATA[ ## Vulnerability Details **File Location**: `scripts/audit_page.py`, lines 138–153 and 286–299 **Vulnerability Type**: Server-Side Request Forgery (SSRF) through unvalidated redirects **Risk Level**: High ### Vulnerable Code ```python def fetch(url): session = requests.Session() session.headers.update({"User-Agent": UA}) response = session.get(url, timeout=TIMEOUT, allow_redirects=True) response.raise_for_status() return response, session def fetch_optional(session, url): try: r = session.get(url, timeout=TIMEOUT, allow_redirects=True) return { "url": r.url, "status": r.status_code, "ok": 200 <= r.status_code < 300, "text": r.text[:4000], } except Exception as e: return {"url": url, "ok": False, "error": str(e)} ``` The initial target validation is performed only before `fetch()`: ```python url = sys.argv[1] blocked, reason = is_blocked_target(url) if blocked: print( f"Refusing to audit target by default: {reason}. Use only public HTTP/HTTPS URLs for routine SEO audits.", file=sys.stderr, ) sys.exit(3) response, session = fetch(url) final_url = response.url soup = BeautifulSoup(response.text, "html.parser") ``` ### Technical Analysis The script attempts to prevent SSRF by rejecting private, loopback, link-local, reserved, multicast, and known metadata destinations in `is_blocked_target()`. The strings `metadata.google.internal` and `169.254.169.254` are denylist entries and do not themselves represent intentional metadata access. However, both network request functions use `allow_redirects=True`. The supplied URL is validated only once, before the first request. The `requests` library then follows HTTP redirects automatically without passing each redirect destination through `is_blocked_target()`. Consequently, an attacker-controlled public server can pass the initial validation and return a redirect to: - ...[truncated 2936 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Disable automatic redirects** Set `allow_redirects=False` for every request and process redirects manually. 2. **Validate every redirect destination** Resolve relative `Location` headers with `urljoin()`, then apply `is_blocked_target()` before issuing the next request. Reject redirects to private, loopback, link-local, reserved, multicast, and metadata destinations. 3. **Enforce a strict redirect limit** Permit only a small number of redirects, such as five, and reject loops. 4. **Restrict URL schemes** Explicitly allow only `http` and `https`. Reject URLs containing credentials and reject all unsupported schemes. 5. **Revalidate final destinations** Validate `response.url` before parsing content or using it to construct robots and sitemap URLs. 6. **Mitigate DNS rebinding** Avoid resolving a hostname during validation and then independently resolving it during connection. Use a networking layer that connects to a previously validated IP address while preserving the expected HTTP Host header and TLS SNI, or apply equivalent connection-time IP enforcement. 7. **Apply identical protections to all requests** Page, robots, sitemap, and sitemap-index requests must share the same redirect, DNS, scheme, and destination validation logic. 8. **Constrain responses** Stream responses and enforce maximum byte limits before downloading complete bodies. Accept only expected textual content types and avoid returning raw internal-looking response bodies. A safe redirect loop should follow this general pattern: ```python def safe_get(session, url, max_redirects=5): current = url for _ in range(max_redirects + 1): blocked, reason = is_blocked_target(current) if blocked: raise ValueError(f"Blocked request destination: {reason}") response = session.get( current, timeout=TIMEOUT, allow_redirects=False, stream=T ...[truncated 642 chars]
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (4)

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding
The code does perform part of the declared SEO auditing purpose: on-page and some technical SEO checks for a supplied URL. However, the description substantially overstates breadth and depth. There is no implementation for local SEO, GBP gaps, keyword research, backlink analysis, content strategy beyond simple word count, or any execution/remediation actions. The code also does not crawl a site broadly; it fetches one page and optionally robots.txt and sitemap endpoints. Network access to public URLs is consistent with SEO auditing, and the host/IP blocking is a supporting safety measure rather than a mismatch. Overall, the declared description is materially broader than the actual behavior.

Cloud Metadata Access

High
Category
Server-Side Request Forgery
Content
TIMEOUT = 20
BLOCKED_HOSTS = {
    "localhost",
    "metadata.google.internal",
}
BLOCKED_IPS = {
    "127.0.0.1",
Confidence
90% confidence
Finding
Code accesses a cloud instance metadata endpoint (e.g. 169.254.169.254). A single request can return temporary IAM credentials, making this a high-value SSRF target for credential theft.

Cloud Metadata Access

High
Category
Server-Side Request Forgery
Content
"127.0.0.1",
    "0.0.0.0",
    "::1",
    "169.254.169.254",
}
Confidence
90% confidence
Finding
Code accesses a cloud instance metadata endpoint (e.g. 169.254.169.254). A single request can return temporary IAM credentials, making this a high-value SSRF target for credential theft.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Static analysis

No suspicious patterns detected.