Back to skill

Security audit

AI Product Radar

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent AI product monitoring purpose, but its screenshot and feed handling create review-worthy risks where malicious feed content or unpinned browser tooling could execute code or access internal network locations.

Do not install this version for routine use without review or fixes. At minimum, disable screenshots with `--no-screenshots` or run it in a locked-down sandbox, avoid untrusted custom feeds, pin and install Playwright explicitly outside normal execution, validate URLs to public http/https destinations, serialize URL/path values instead of embedding them in code, and escape all generated HTML and Markdown output.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/ai_product_radar.py:332
Finding

RSS-Controlled JavaScript Injection Enables Arbitrary Local Command Execution

Content
View full analysis
bool: try: import subprocess script = f""" const {{ chromium }} = require('playwright'); (async () => {{ const browser = await chromium.launch(); const page = await browser.newPage({viewport: {{width: 1280, height: 800}}}); await page.goto('{url}', {{waitUntil: 'domcontentloaded', timeout: 20000}}); await page.waitForTimeout(2000); await page.screenshot({{path: '{path}', fullPage: false}}); await browser.close(); }})(); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=45) return result.returncode == 0 and path.exists() except Exception: return False ``` ### Technical Analysis The `url` value originates from an RSS or Atom entry link and is directly interpolated into JavaScript source code enclosed by single quotes. The generated source is then executed using `node -e`. An attacker-controlled URL containing a single quote can terminate the `page.goto()` string and inject arbitrary JavaScript. Because Node.js exposes operating-system functionality through modules such as `child_process`, successful injection can be converted into arbitrary command execution under the account running the Skill. The screenshot path is also interpolated into JavaScript without serialization. Although it is primarily derived from the user-selected output directory and a sanitized filename, it remains an additional injection surface if the output path contains JavaScript metacharacters. Passing the generated source as an argument rather than through a shell does not prevent this vulnerability: the injec ...[truncated 1448 chars]
Remediation
View remediation
{ const url = process.argv[1]; const outputPath = process.argv[2]; const browser = await chromium.launch(); try { const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 }); await page.waitForTimeout(2000); await page.screenshot({ path: outputPath, fullPage: false }); } finally { await browser.close(); } })(); """ subprocess.run( ["node", "-e", node_script, url, str(path)], capture_output=True, text=True, timeout=45, check=False, ) ``` 4. Validate links before browser navigation. Permit only explicitly supported `http` and `https` URLs. 5. Combine URL validation with private-address and redirect restrictions to address the SSRF risk described separately. 6. Add regression tests containing quotes, backslashes, line separators, and JavaScript fragments in RSS entry links and output paths. ]]>

T08 · Insecure Dependencies

Error
Location
scripts/ai_product_radar.py:294
Finding

Automatic Download and Execution of an Unpinned npm Package

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/ai_product_radar.py:87
Finding

Unrestricted Feed Fetching and Browser Navigation Permit SSRF

Content
View full analysis
bytes: req = urllib.request.Request(url, headers={ "User-Agent": "AIProductRadar/1.0 (+https://github.com/openclaw)", "Accept": "*/*", }) with urllib.request.urlopen(req, timeout=timeout) as resp: return resp.read() ``` ```python def monitor_rss(feeds: list[str], days: int, query: str | None = None) -> list[dict]: print("📡 Stage 1: Monitoring RSS feeds...") all_items = [] for url in feeds: print(f" Fetching {url} ...") try: data = fetch_url(url) items = parse_rss_feed(data, url, days) print(f" → {len(items)} items") all_items.extend(items) except Exception as e: print(f" ⚠ Error: {e}", file=sys.stderr) ``` ```python def _screenshot_playwright(url: str, path: Path) -> bool: try: import subprocess script = f""" const {{ chromium }} = require('playwright'); (async () => {{ const browser = await chromium.launch(); const page = await browser.newPage({viewport: {{width: 1280, height: 800}}}); await page.goto('{url}', {{waitUntil: 'domcontentloaded', timeout: 20000}}); await page.waitForTimeout(2000); await page.screenshot({{path: '{path}', fullPage: false}}); await browser.close(); }})(); """ result = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=45) return result.returncode == 0 and path.exists() except Exception: return False ``` ```python feeds = DEFAULT_FEEDS if args.feeds: feeds = json.loads(Path(args.feeds) ...[truncated 2510 chars]
Remediation
View remediation
None: parsed = urllib.parse.urlparse(url) if parsed.scheme not in {"http", "https"}: raise ValueError("Only HTTP and HTTPS URLs are allowed") if not parsed.hostname: raise ValueError("URL must contain a hostname") for result in socket.getaddrinfo(parsed.hostname, parsed.port): address = ipaddress.ip_address(result[4][0]) if not address.is_global: raise ValueError("Non-public destination is not allowed") ``` This check must be supplemented with redirect validation and connection-level protections rather than being treated as a complete SSRF defense by itself. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ai_product_radar.py:354
Finding

Untrusted RSS Content Is Written to Active HTML and Markdown Without Output Encoding

Content
View full analysis
body {{ margin:0; padding:40px; font-family:system-ui,sans-serif; background:linear-gradient(135deg,#1a1a2e,#16213e); color:#eee; width:1200px; height:720px; display:flex; flex-direction:column; justify-content:center; box-sizing:border-box; }} h1 {{ font-size:2.2em; margin:0 0 12px; color:#00d4ff; }} .source {{ color:#888; font-size:0.9em; margin-bottom:20px; }} .desc {{ font-size:1.1em; line-height:1.6; color:#ccc; max-width:900px; }} .tags {{ margin-top:24px; }} .tag {{ display:inline-block; background:#0f3460; padding:6px 14px; border-radius:20px; margin-right:8px; font-size:0.85em; color:#e94560; }} a {{ color:#00d4ff; word-break:break-all; }}

{item.get('product_name', item.get('title', 'Unknown'))}

{item.get('source', '')} · {item.get('domain', '')}
{item.get('description', 'No description available.')}
{''.join(f'{c}' for c in item.get('categories', []))}

{item.get('link', '')}

""" html_path.write_text(html, encoding="utf-8") # Also save a tiny placeholder PNG indicator path.write_bytes(b"") # empty file signals placeholder item["screenshot_html"] = str(html_path) ``` ```python for i, p in enumerate(report["trending"][:15], 1): cats = ", ".join(p.get("catego ...[truncated 3198 chars]
Remediation
View remediation
str: return html.escape(str(value or ""), quote=True) def safe_link(value: str) -> str: parsed = urllib.parse.urlparse(value or "") if parsed.scheme not in {"http", "https"}: return "" return escaped(value) product_name = escaped(item.get("product_name", item.get("title", "Unknown"))) source = escaped(item.get("source", "")) domain = escaped(item.get("domain", "")) description = escaped(item.get("description", "No description available.")) link = safe_link(item.get("link", "")) categories = [escaped(c) for c in item.get("categories", [])] ``` For Markdown, either use a well-tested escaping library or constrain untrusted values to plain text by escaping backslashes, brackets, parentheses, backticks, heading markers, angle brackets, and line breaks. ]]>
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 (7)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill advertises executable behavior involving network access, filesystem reads/writes, and shell execution, but it does not declare any explicit tool scope or permissions boundary. That creates an authorization and review gap: the agent may invoke powerful capabilities implicitly, making it harder for operators to constrain the skill or understand its real blast radius before use.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The trigger list ends with broad language like 'or similar product intelligence workflows,' which can cause the skill to activate in contexts beyond the user's precise intent. Overbroad activation increases the chance that a networked, file-writing, shell-capable skill runs unexpectedly, expanding exposure to unintended external requests or local side effects.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The script automatically fetches RSS feeds in Stage 1 and later visits product links for screenshot capture, which transmits network requests to third-party services. While the code logs progress, it does not clearly disclose to the user that running the tool will contact external sites and potentially expose the runner's IP and request metadata.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The screenshot stage automatically visits third-party URLs from feeds and writes local files without meaningful user warning or strong safety controls. In this context, that is risky because the visited URLs are externally sourced and can drive browser-based requests to arbitrary destinations, increasing exposure to SSRF-like access to internal resources and compounding the unsafe node -e construction.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The manifest describes RSS monitoring, enrichment, screenshot capture, and trend analysis, but the implementation achieves screenshots by probing for npx playwright and then running node -e with dynamically constructed JavaScript. Spawning external executables and browser automation frameworks is a significant execution capability that is not clearly justified by a simple monitoring/reporting skill description, especially since screenshot capture could be implemented via a declared dedicated service instead of arbitrary local tool execution.

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/ai_product_radar.py (reported line 307)May include surrounding context.

python
has_puppeteer = False
    try:
        import subprocess
        result = subprocess.run(["npx", "--yes", "playwright", "--version"],
                                capture_output=True, text=True, timeout=30)
        has_playwright = result.returncode == 0
    except Exception:

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
88% confidence
Finding

The script builds a JavaScript snippet with unescaped attacker-controlled data (url and filesystem path) and passes it to node -e. Because feed contents can supply arbitrary links, a crafted URL containing quotes can break out of the JavaScript string and inject arbitrary Node/Playwright code, resulting in local code execution under the user running the skill.

Content

Scanner excerpt · scripts/ai_product_radar.py (reported line 354)May include surrounding context.

python
await browser.close();
        }})();
        """
        result = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=45)
        return result.returncode == 0 and path.exists()
    except Exception:
        return False