Back to skill

Security audit

Web Markdown Navigator

Security checks for vulnerabilities and agentic risk

Overview

The skill has a legitimate webpage-to-markdown purpose, but its fetcher can be abused to reach internal network resources from the agent environment.

Review before installing. This skill should only be used in environments where outbound web fetches cannot reach private networks, localhost services, cloud metadata endpoints, or other sensitive internal systems. Prefer fixing URL validation and redirect handling, updating the vulnerable dependency, and adding explicit tool/network scoping before broad use.

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/fetch-markdown.mjs:15
Finding
Bypassable SSRF Protection Allows Access to Internal Network Resources<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-markdown.mjs:15-28, 121-140` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```js function isPrivateHost(hostname) { const h = hostname.toLowerCase(); if (["localhost", "127.0.0.1", "::1", "0.0.0.0"].includes(h)) return true; const ipv4 = h.match(/^(\d{1,3}\.){3}\d{1,3}$/); if (!ipv4) return false; const [a, b] = h.split(".").map(Number); if (a === 10) return true; if (a === 127) return true; if (a === 169 && b === 254) return true; if (a === 172 && b >= 16 && b <= 31) return true; if (a === 192 && b === 168) return true; return false; } ``` ```js const res = await fetch(u, { signal: ac.signal, redirect: "follow", headers: { "user-agent": "web-markdown-navigator/1.0 (+OpenClaw skill)", accept: "text/html,application/xhtml+xml", }, }); fetchedUrl = res.url || fetchedUrl; const finalUrl = new URL(fetchedUrl); if (isPrivateHost(finalUrl.hostname)) { printErr("ERROR: redirected to private/local host"); process.exit(2); } ``` ### Technical Analysis The URL safety check only blocks selected literal IPv4 addresses and the exact hostnames `localhost`, `127.0.0.1`, `::1`, and `0.0.0.0`. It does not resolve hostnames and validate their A and AAAA records before establishing a connection. Consequently, a public-looking hostname that resolves to a private, loopback, link-local, reserved, or cloud metadata address passes validation. The check also lacks comprehensive IPv6 filtering. IPv6 loopback aliases, unique-local addresses, link-local addresses, IPv4-mapped IPv6 addresses, and other non-public address forms are not rejected. Redirect processing is unsafe because `redirect: "follow"` permits the HTTP client to contact every redirect destination automatically. The final URL is checked only after the redirect chain has already been followed and the destination has been contacted. Although the respo ...[truncated 1990 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve the destination hostname before making a request and validate every returned A and AAAA address. 2. Reject all loopback, private, link-local, multicast, unspecified, reserved, documentation, carrier-grade NAT, and cloud metadata address ranges for both IPv4 and IPv6. 3. Disable automatic redirects by using `redirect: "manual"`. 4. For each redirect, resolve and validate the new destination before sending the next request. 5. Limit the number of redirects and reject protocol changes or destinations containing unexpected credentials. 6. Protect against DNS rebinding by binding the connection to the exact address that was validated rather than resolving the hostname again during connection establishment. 7. Apply outbound firewall or proxy rules that deny access to internal and metadata networks, providing defense in depth. 8. Consider an explicit allowlist of permitted public domains when the deployment context permits it. 9. Add regression tests covering: - Hostnames resolving to private IPv4 addresses. - IPv6 loopback, unique-local, and link-local addresses. - IPv4-mapped IPv6 addresses. - Public-to-private redirect chains. - Multi-step redirects. - DNS rebinding scenarios. ]]>
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)

Ae1

High
Category
analysis-evasion
Content
`node scripts/fetch-markdown.mjs <url> [--max-chars N] [--timeout-ms N] [--json]`
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Known Vulnerable Dependency: ws==8.19.0 — 2 advisory(ies): CVE-2026-45736 (ws: Uninitialized memory disclosure); CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
94% confidence
Finding
The lockfile pins jsdom’s transitive dependency ws to 8.19.0, which the provided advisories identify as affected by memory disclosure and memory-exhaustion denial of service issues. In this skill’s context, the package processes untrusted remote web content, so a vulnerable WebSocket library in the dependency tree increases risk if WebSocket functionality is reachable through jsdom or related parsing/fetch paths, though it is not obviously the skill’s primary feature.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill instructs the agent to execute a network-capable script, but the manifest does not declare any explicit tool scope such as allowed tools or permissions. This creates an authorization gap where the skill’s effective capabilities are broader or less auditable than its declared interface, increasing the risk of unintended outbound requests or misuse in environments that rely on manifest-level restrictions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"type": "module",
  "version": "1.0.0",
  "dependencies": {
    "@mozilla/readability": "^0.6.0",
    "jsdom": "^26.1.0",
    "turndown": "^7.2.0"
  }
Confidence
93% confidence
Finding
The dependency uses a caret range (^0.6.0), which allows semver-compatible updates to be installed rather than a single fixed version. In a supply-chain context this can introduce unexpected code changes or a compromised upstream release into the build, though package.json alone does not prove exploitation and the risk is somewhat mitigated when a lockfile is used.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"version": "1.0.0",
  "dependencies": {
    "@mozilla/readability": "^0.6.0",
    "jsdom": "^26.1.0",
    "turndown": "^7.2.0"
  }
}
Confidence
93% confidence
Finding
The jsdom dependency is specified with a caret range (^26.1.0), so future installs may resolve to newer allowed versions without explicit review. That increases supply-chain risk and can also introduce security or behavior regressions into a skill that processes untrusted web content.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {
    "@mozilla/readability": "^0.6.0",
    "jsdom": "^26.1.0",
    "turndown": "^7.2.0"
  }
}
Confidence
92% confidence
Finding
The turndown dependency is unpinned via a caret range (^7.2.0), permitting automatic uptake of later compatible releases. While common in development, this weakens reproducibility and creates a low-grade supply-chain exposure if an upstream version is vulnerable or malicious.