Back to skill

Security audit

Html2md

Security checks for vulnerabilities and agentic risk

Overview

This skill appears to do what it claims, but it should be reviewed carefully because it can fetch any reachable URL and includes a flagged HTTP dependency.

Install only if callers and workflows are trusted, or run it in a sandbox with egress blocked to private networks and metadata services. Validate URLs before passing them to the tool, update or audit dependencies before use, and treat converted markdown from remote pages as untrusted quoted content.

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/html2md.js:77
Finding
Unrestricted URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/html2md.js:77-109`, with the user-controlled URL reaching the function at `scripts/html2md.js:321-324` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```js async function fetchHtml(url) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 15000); let response; try { response = await fetch(url, { signal: controller.signal, redirect: 'follow', headers: { 'User-Agent': 'html2md/1.0 (agent-friendly HTML converter; +https://github.com/openclaw/html2md)', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.5', }, }); } catch (err) { if (err.name === 'AbortError') die(`Timeout: request exceeded 15s for ${url}`); const cause = err.cause?.message || err.message; die(`Network error fetching ${url}: ${cause}`); } finally { clearTimeout(timer); } if (!response.ok) die(`HTTP ${response.status} ${response.statusText} — ${url}`); const ct = response.headers.get('content-type') || ''; if (!ct.includes('html') && !ct.includes('xml') && !ct.includes('text')) { die(`Non-HTML content type: ${ct} — use a different tool for binary content`); } return { html: await response.text(), finalUrl: response.url }; } ``` The untrusted URL reaches this function directly: ```js } else if (url) { const result = await fetchHtml(url); html = result.html; pageUrl = result.finalUrl; } ``` ### Technical Analysis The CLI fetches a caller-provided URL without validating its scheme, hostname, resolved IP addresses, port, or network destination. It also enables automatic redirects through `redirect: 'follow'` without validating each redirect target. This behavior is necessary at a basic level because URL retrieval is part of the declared HTML conversion functionality. However, ...[truncated 1962 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only explicitly supported schemes, normally `https:` and optionally `http:`. 2. Reject URLs containing embedded credentials or otherwise unexpected URL components. 3. Resolve the hostname before connecting and reject every address in prohibited ranges, including: - IPv4 and IPv6 loopback. - RFC1918 and IPv6 unique-local addresses. - Link-local addresses. - Multicast, unspecified, reserved, and documentation ranges. - Known cloud metadata destinations. 4. Prevent DNS rebinding by ensuring the validated address is the address used for the connection, or enforce destination controls at the network layer. 5. Replace automatic redirect following with manual redirect processing. Parse, resolve, and validate every redirect target before issuing the next request. 6. Apply an explicit destination allowlist when the expected set of websites is known. 7. Run the Skill in a sandbox whose egress policy denies private networks and metadata services. 8. Retain the timeout, and additionally impose response-size and redirect-count limits to reduce resource-exhaustion risks. 9. Document that untrusted users must not be allowed to choose arbitrary destinations unless these controls are enabled. ]]>

other

Warning
Location
scripts/html2md.js:124
Finding
Untrusted Web Content Is Emitted for Agent Consumption Without a Trust Boundary<![CDATA[ ## Vulnerability Details **File Location**: `scripts/html2md.js:124-151` and `scripts/html2md.js:326-363` **Vulnerability Type**: Indirect Prompt Injection **Risk Level**: Medium ### Vulnerable Code The extraction logic preserves ordinary page text: ```js function extractContent(html, pageUrl) { const dom = new JSDOM(html, { url: pageUrl || 'http://localhost/' }); const title = dom.window.document.title || ''; const reader = new Readability(dom.window.document.cloneNode(true), { charThreshold: 0, keepClasses: false, }); const article = reader.parse(); // Quality check: if Readability returns too little content, fall back to body const MIN_WORDS = 30; const wordCount = (str) => str ? str.trim().split(/\s+/).length : 0; if (!article || wordCount(article.content) < MIN_WORDS) { return { title, content: bodyFallback(dom), }; } return { title: article.title || title, content: article.content || '', }; } ``` The extracted text is converted and emitted without an explicit untrusted-content boundary: ```js // Extract const extracted = extractContent(html, pageUrl); title = extracted.title; // Convert const td = buildTurndown(opts); let markdown = td.turndown(extracted.content); // Post-process markdown = postProcess(markdown, opts); // Prepend title if not already first heading if (title && !markdown.startsWith('# ')) { markdown = `# ${title}\n\n${markdown}`; } // Token budget if (opts.maxTokens) { markdown = truncateToTokens(markdown, opts.maxTokens); } const tokens = countTokens(markdown); // Output if (opts.json) { const out = { title, url: pageUrl, markdown, tokens, }; process.stdout.write(JSON.stringify(out, null, 2) + '\n'); } else { process.stdout.write(markdown + '\n'); } ``` ### Technical Analysis Readability and Turndown remove or transform presentation elements, but they intentionally preserve the textual content of a page. Consequently, attacke ...[truncated 2224 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Mark converted content explicitly as untrusted external data in both plain-text and JSON output modes. 2. Add structured provenance fields such as source URL, retrieval time, trust level, and content type. 3. Delimit the converted page body clearly so that integrations can keep it separate from system, developer, and user instructions. 4. Update the Skill documentation to state that instructions found in converted content must never override the calling agent's task or safety policies. 5. Require downstream agents to treat fetched content as quoted evidence, not executable instructions. 6. Enforce authorization for sensitive tool calls outside the language model. Do not rely exclusively on prompt-based warnings. 7. Prevent untrusted page content from being written directly into persistent memory or reused as policy without validation. 8. Where practical, detect and flag likely instruction-like content. Such detection should be supplementary because text filtering cannot reliably eliminate prompt injection. 9. Apply least privilege to the consuming agent so that successful prompt manipulation cannot access unrelated secrets, files, networks, or mutation-capable tools. ]]>
Vulnerability Patterns
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (6)

Known Vulnerable Dependency: undici==7.22.0 — 16 advisory(ies): CVE-2026-1525 (Undici has an HTTP Request/Response Smuggling issue); CVE-2026-6733 (undici vulnerable to HTTP response queue poisoning via keep-alive socket reuse); CVE-2026-1527 (Undici has CRLF Injection in undici via `upgrade` option) +13 more

High
Category
Supply Chain
Confidence
98% confidence
Finding
The lockfile pins jsdom's transitive dependency undici to 7.22.0, and the supplied finding indicates multiple known high-severity advisories affecting that exact version, including request/response smuggling, response queue poisoning, and CRLF injection. In this skill's context, html2md explicitly supports URL fetching and web scraping, so vulnerable HTTP client behavior is security-relevant because untrusted remote servers could potentially manipulate request/response handling or poison reused connections.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill explicitly supports fetching arbitrary URLs, which is a network-capable behavior, but its manifest does not declare any tool scope such as permissions or allowed-tools. In an agent environment, undeclared network access weakens policy enforcement and reviewability, and can enable unintended outbound requests or SSRF-style misuse when user-controlled URLs are passed through the skill.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The request headers hard-code `Accept-Language: 'en-US,en;q=0.5'`, which imposes a specific language/locale preference for fetched content. This is a natural-language policy concern because the tool does not offer user opt-in or configuration for locale selection.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"author": "",
  "license": "ISC",
  "dependencies": {
    "@mozilla/readability": "^0.6.0",
    "jsdom": "^28.1.0",
    "turndown": "^7.2.2"
  }
Confidence
93% confidence
Finding
The dependency is specified with a caret range, which allows newer compatible versions to be installed over time rather than a single immutable version. This can introduce supply-chain risk through unexpected upstream changes or compromised releases, although by itself it does not indicate malicious behavior and is common in normal JavaScript projects.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "ISC",
  "dependencies": {
    "@mozilla/readability": "^0.6.0",
    "jsdom": "^28.1.0",
    "turndown": "^7.2.2"
  }
}
Confidence
93% confidence
Finding
The jsdom dependency is declared with a caret version range, so installations may resolve to different package versions over time. In a tool that processes untrusted HTML and may fetch remote content, dependency drift increases supply-chain exposure because security posture can change without a code change in this repository.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "@mozilla/readability": "^0.6.0",
    "jsdom": "^28.1.0",
    "turndown": "^7.2.2"
  }
}
Confidence
92% confidence
Finding
Using a caret range for turndown permits automatic uptake of future patch and minor releases, which reduces build reproducibility and increases exposure to compromised or breaking upstream releases. This is a genuine but low-severity supply-chain hardening issue rather than an immediate exploit in the file itself.