T09 · Insecure Skill Coding Practices
- Location
kpn_call.mjs:40- Finding
Unvalidated Server-Provided Polling URL Bypasses the Declared Network Boundary
- Content
View full analysis
Vulnerability Details
File Location:
kpn_call.mjs, lines 40–46
Vulnerability Type: Server-Side Request Forgery (SSRF) through an unvalidated remote URL
Risk Level: MediumVulnerable Code
js // Step 2 — poll status_url until the verdict is ready (HTTP 200). const statusUrl = started.status_url; // already carries the access token const pollMs = Math.max(15000, Number(opts.pollMs) || 45000); const deadline = Date.now() + (Math.max(60000, Number(opts.timeoutMs) || 20 * 60 * 1000)); while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, pollMs)); const r = await fetch(statusUrl, { headers: { Accept: "application/json" } });Technical Analysis
The initial advisory response controls
started.status_url. The implementation passes this value directly tofetch()without verifying its protocol, hostname, port, embedded credentials, or destination address.The Skill declares network access only to
persona-mcp-server.onrender.com, but this validation is not enforced in code. A compromised or malicious advisory server could therefore provide a URL pointing to an unrelated public host, a loopback service, a private-network endpoint, or a cloud metadata service.Redirect behavior is also not restricted. Even if the initial URL were validated, an allowed endpoint could redirect the polling request to a disallowed destination unless redirects are explicitly rejected or every redirect target is validated.
This behavior exceeds the minimum network privileges declared by the Skill.
Attack Path
- An attacker compromises, impersonates, or gains control over the advisory service response.
- The Agent submits a legitimate question to:
https://persona-mcp-server.onrender.com/ai/advisory?src=clawhub. - The malicious response contains a valid-looking
status_urltargeting an internal or otherwise unauthorized endpoint. - The implementation assigns that value directly to
statusUrl. - After ...[truncated 1078 chars]
- Remediation
View remediation
Remediation Suggestions
Validate the polling URL before any request:
- Parse it using
new URL(started.status_url). - Require the
https:protocol. - Require an exact hostname match for
persona-mcp-server.onrender.com. - Reject embedded usernames and passwords.
- Reject unexpected ports.
- Reject malformed URLs and URLs whose origin differs from the configured API origin.
- Set
redirect: "error"on submission and polling requests, or manually validate every redirect destination. - Where possible, do not trust a complete URL from the response. Accept only a reference identifier or relative path and construct the polling URL from the hardcoded trusted origin.
- Enforce the network allowlist at the runtime or sandbox layer as defense in depth.
Example hardening:
js const allowedOrigin = new URL(API).origin; const parsedStatusUrl = new URL(started.status_url); if ( parsedStatusUrl.protocol !== "https:" || parsedStatusUrl.origin !== allowedOrigin || parsedStatusUrl.username || parsedStatusUrl.password ) { return { status: "error", message: "invalid status URL" }; } const r = await fetch(parsedStatusUrl.href, { headers: { Accept: "application/json" }, redirect: "error", });The initial submission request should likewise use
redirect: "error"to prevent redirection outside the declared network boundary.- Parse it using
