Back to skill

Security audit

Seo Geo Qa

Security checks for vulnerabilities and agentic risk

Overview

This SEO QA skill mostly matches its stated purpose, but it can automatically contact arbitrary links and send full URLs or keywords to third-party services, which needs review before use.

Install only if you are comfortable with the skill contacting every extracted or supplied URL and sharing SEO keywords or full URLs with external services such as DuckDuckGo and r.jina.ai. Avoid using it on confidential drafts, preview links, signed URLs, intranet URLs, localhost/private-network addresses, or cloud metadata paths unless you run it in a network-restricted sandbox or add URL allowlist and private-address blocking controls.

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/verify_links.py:113
Finding
Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Locations**: - `scripts/verify_links.py:113-130, 221-228` - `scripts/serp_gap_analyzer.py:111-136, 225-232, 310-316` - `scripts/post_publish_check.py:59-63, 75-79` **Vulnerability Type**: Server-Side Request Forgery (SSRF) through unvalidated URLs **Risk Level**: High ### Vulnerable Code `scripts/verify_links.py:113-130`: ```python def curl_head(url: str) -> str: cmd = [ "curl", "-sI", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "10", "-L", "-A", USER_AGENT, url, ] try: out = subprocess.check_output(cmd, text=True).strip() return out or "000" except Exception: return "000" def fetch(url: str) -> tuple[str, str, str]: req = Request(url, headers={"User-Agent": USER_AGENT}) try: with urlopen(req, timeout=TIMEOUT) as resp: body = resp.read(120000).decode("utf-8", errors="ignore") return str(resp.status), resp.geturl(), body ``` `scripts/verify_links.py:221-228`: ```python def verify_url(url: str) -> LinkResult: domain = normalize_domain(url) http_status = curl_head(url) result = LinkResult(url=url, domain=domain, http_status=http_status) if http_status in {"404", "410"}: result.verdict = "dead" result.evidence = f"HTTP {http_status}" ``` `scripts/serp_gap_analyzer.py:111-136`: ```python def fetch(url: str): req = Request(url, headers={"User-Agent": USER_AGENT}) try: with urlopen(req, timeout=TIMEOUT) as resp: body = resp.read(180000).decode("utf-8", errors="ignore") return str(resp.status), resp.geturl(), body except HTTPError as e: body = e.read(180000).decode("utf-8", errors="ignore") if hasattr(e, "read") else "" return str(e.code), url, body except URLError: return "000", url, "" except Exception: return "000", url, "" def jina_fetch(url: str) -> str: """Fetch a URL via Jina Reader (r.ji ...[truncated 4128 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only `http` and `https` URLs and reject URLs containing user information. 2. Resolve the hostname before each request and reject every address classified as loopback, private, link-local, multicast, unspecified, or reserved. 3. Explicitly block common metadata destinations, including `169.254.169.254`, even if other address checks are present. 4. Disable automatic redirects and validate each redirect target before following it. 5. Re-resolve and revalidate each destination immediately before connecting to reduce DNS-rebinding risk. 6. Restrict destination ports to an approved set such as 80 and 443 unless the user explicitly authorizes another port. 7. Consider an optional domain allowlist for controlled publishing environments. 8. Run network checks in a sandbox with no access to cloud metadata, localhost services, or private application networks. 9. Apply the same validation helper consistently to `curl`, `urlopen`, Jina target URLs, and all direct-fetch fallbacks. 10. Add regression tests for direct private IPs, IPv6 loopback, encoded addresses, DNS rebinding, and public-to-private redirects. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/verify_links.py:155
Finding
Complete URLs May Be Disclosed to External Search and Proxy Services<![CDATA[ ## Vulnerability Details **File Locations**: - `scripts/verify_links.py:155-159` - `scripts/serp_gap_analyzer.py:126-135, 146-174, 225-232` **Vulnerability Type**: Sensitive URL disclosure to third-party services **Risk Level**: Medium ### Vulnerable Code `scripts/verify_links.py:155-159`: ```python def search_index(domain: str, url: str) -> tuple[bool, str]: url = canonicalize_url(url) query = quote(f'site:{domain} "{url}"') search_url = f"https://html.duckduckgo.com/html/?q={query}" status, final_url, body = fetch(search_url) ``` `scripts/serp_gap_analyzer.py:126-135`: ```python def jina_fetch(url: str) -> str: """Fetch a URL via Jina Reader (r.jina.ai) and return Markdown content. Bypasses Cloudflare and other anti-bot measures using a real browser renderer. No API key required for basic usage.""" jina_url = JINA_BASE + url req = Request(jina_url, headers={"User-Agent": USER_AGENT, "Accept": "text/markdown"}) try: with urlopen(req, timeout=JINA_TIMEOUT) as resp: return resp.read(500000).decode("utf-8", errors="ignore") except Exception: return "" ``` `scripts/serp_gap_analyzer.py:146-174`: ```python def ddg_search(query: str, limit: int) -> list[str]: search_url = f"https://html.duckduckgo.com/html/?q={quote(query)}" SKIP_DOMAINS = {"duckduckgo.com", "youtube.com", "facebook.com", "instagram.com", "x.com", "r.jina.ai"} urls = [] seen = set() # Primary: direct urllib fetch (fast path, no extra latency) status, _, body = fetch(search_url) if status.startswith("2") and body: parser = SERPParser() parser.feed(body) for link in parser.links: domain = normalize_domain(link) if any(x in domain for x in SKIP_DOMAINS): continue if link not in seen: seen.add(link) urls.append(link) if len(urls) >= limit: break # Fal ...[truncated 3235 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not include complete URLs in search-engine queries. Search by normalized hostname and a non-sensitive path component only when necessary. 2. Remove user information and fragments, and default to removing all query parameters before transmitting a URL to an external service. 3. If query parameters are essential, maintain a conservative allowlist rather than a blocklist of known tracking keys. 4. Detect likely secrets using parameter names such as `token`, `key`, `signature`, `sig`, `auth`, `code`, `session`, and `expires`; refuse third-party transmission when detected. 5. Require explicit user consent before sending target URLs to Jina or another rendering proxy. 6. Make direct fetching the default and proxy fetching an explicit opt-in for each run. 7. Propagate a `--no-jina` or stricter offline/direct-only setting through `seo_qa_runner.py`. 8. Display a clear warning identifying which external services receive keywords and URLs. 9. Avoid submitting intranet, localhost, private-address, or non-public hostnames to external services. 10. Document applicable third-party retention and privacy considerations for environments that handle confidential drafts. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (17)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill is presented as a comprehensive draft QA tool, yet the findings indicate it lacks implementation for several promised checks and relies on external network tools not reflected in declared permissions. In a publishing workflow, that can cause teams to approve content based on incomplete validation and can silently introduce data-handling or compliance issues through unannounced external requests.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill is presented as a comprehensive draft QA tool, yet the findings indicate it lacks implementation for several promised checks and relies on external network tools not reflected in declared permissions. In a publishing workflow, that can cause teams to approve content based on incomplete validation and can silently introduce data-handling or compliance issues through unannounced external requests.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The skill is presented as a comprehensive draft QA tool, yet the findings indicate it lacks implementation for several promised checks and relies on external network tools not reflected in declared permissions. In a publishing workflow, that can cause teams to approve content based on incomplete validation and can silently introduce data-handling or compliance issues through unannounced external requests.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises and instructs use of filesystem access, shell execution, and outbound network activity, but it does not declare any explicit tool scope or permissions. That creates a transparency and governance gap: users or orchestrators may invoke a skill with broader capabilities than expected, increasing the chance of unintended file modification, data exposure, or unreviewed network access.

Vague Triggers

Medium
Confidence
94% confidence
Finding
An overly broad trigger like 'check this article' can cause accidental invocation in normal editorial conversation, leading the skill to run unexpectedly with file, shell, and network-capable behavior. In this context that is not a direct exploit primitive, but it increases the likelihood of unintentional execution and unnecessary exposure of local content or URLs to external services.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_json(cmd: list[str], allow_failure: bool = False) -> dict | None:
    proc = subprocess.run(cmd, capture_output=True, text=True)
    if proc.returncode != 0:
        if allow_failure:
            return {"_error": proc.stderr.strip() or proc.stdout.strip() or f"command failed: {' '.join(cmd)}"}
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_json(cmd: list[str], allow_failure: bool = False) -> dict | None:
    proc = subprocess.run(cmd, capture_output=True, text=True)
    if proc.returncode != 0:
        if allow_failure:
            return {"_error": proc.stderr.strip() or proc.stdout.strip() or f"command failed: {' '.join(cmd)}"}
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
The natural-language configuration explicitly assumes English by defining 'Common English function words' and tokenization behavior around English content, but the CLI and documentation do not disclose that the analysis is English-specific. This creates a locale/language policy issue because users are not offered a language choice or informed of the constraint.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The manifest describes a skill for checking a draft’s broken links, source credibility, missing SEO elements, and citation problems before publishing. This file instead performs search-engine result scraping and competitor content gap analysis, including querying DuckDuckGo and extracting common headings/terms from competing pages, which is a different capability than draft QA.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The script sends user-supplied search keywords to DuckDuckGo and may send target URLs to r.jina.ai without clear disclosure or consent. In this skill context, drafts and research targets may be sensitive or unpublished, so leaking them to third-party services can expose confidential editorial plans, client work, or private URLs.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The implemented workflow fetches competitor pages, parses titles/headings/text, computes term overlap, compares word counts, and recommends missing sections. That behavior aligns with competitive SEO research, but not with the manifest’s claimed focus on broken links, weak sources, missing SEO elements, and citation checks for a single article draft.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The manifest promises a stdlib-only skill with no dependencies, but the code imports `subprocess` and later shells out to the system `curl` executable. That means the skill's actual operation relies on an undeclared non-stdlib external tool, which is a direct mismatch with the claimed implementation constraints.

Context-Inappropriate Capability

Medium
Confidence
93% confidence
Finding
The skill sends discovered URLs to external destinations and also queries DuckDuckGo for search-index checks, which causes unreviewed content-derived links to be contacted automatically. In a content QA context this behavior is expected, but it still creates SSRF-like risk against internal services if a draft contains internal, cloud-metadata, or otherwise sensitive URLs, and it leaks reviewed URLs to third parties.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"--max-time", "10", "-L", "-A", USER_AGENT, url,
    ]
    try:
        out = subprocess.check_output(cmd, text=True).strip()
        return out or "000"
    except Exception:
        return "000"
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script automatically fetches each extracted URL and performs search-engine queries without clearly warning the user that article contents will be transmitted to third parties. In this skill, network access is functionally necessary, but the absence of notice and controls makes accidental disclosure of unpublished URLs, internal references, or confidential draft content more dangerous.

Missing User Warnings

Low
Confidence
81% confidence
Finding
This code creates a report directory and writes both JSON and Markdown QA reports to disk, which is a file-write operation covered by the missing-warning rule for code files. While the CLI exposes a report directory option, there is no confirmation prompt, prior user-facing notice, or inline comment/docstring near the write path explaining that files will be created.

Intent-Code Divergence

Low
Confidence
88% confidence
Finding
The docstring and related comments claim the function renders with a real browser and bypasses Cloudflare/anti-bot measures. In reality, this function simply sends a request to the Jina proxy endpoint and returns its response; whatever browser rendering may occur is external and not performed by this code.

Static analysis

No suspicious patterns detected.