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

Content
View full analysis
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
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/verify_links.py:155
Finding

Complete URLs May Be Disclosed to External Search and Proxy Services

Content
View full analysis
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
View remediation
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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/seo_qa_runner.py (reported line 45)May include surrounding context.

python
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)}"}

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/seo_qa_runner.py (reported line 54)May include surrounding context.

python
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)}"}

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/verify_links.py (reported line 119)May include surrounding context.

python
"--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"

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.