Back to skill

Security audit

KPN Korea Content Judgment

Security checks for vulnerabilities and agentic risk

Overview

The skill has a clear Korea-advisory purpose, but users should review it because it sends potentially sensitive questions and contact details to an external service and trusts a server-provided polling URL.

Install only if you are comfortable sending Korea-related business questions to persona-mcp-server.onrender.com. Avoid including secrets or confidential deal terms unless necessary, do not provide email/webhook/wallet details unless you intentionally want notification registration, and prefer a hardened version that validates the polling URL host before fetching it.

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

Warning
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: Medium

Vulnerable 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 to fetch() 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

  1. An attacker compromises, impersonates, or gains control over the advisory service response.
  2. The Agent submits a legitimate question to: https://persona-mcp-server.onrender.com/ai/advisory?src=clawhub.
  3. The malicious response contains a valid-looking status_url targeting an internal or otherwise unauthorized endpoint.
  4. The implementation assigns that value directly to statusUrl.
  5. After ...[truncated 1078 chars]
Remediation
View remediation

Remediation Suggestions

Validate the polling URL before any request:

  1. Parse it using new URL(started.status_url).
  2. Require the https: protocol.
  3. Require an exact hostname match for persona-mcp-server.onrender.com.
  4. Reject embedded usernames and passwords.
  5. Reject unexpected ports.
  6. Reject malformed URLs and URLs whose origin differs from the configured API origin.
  7. Set redirect: "error" on submission and polling requests, or manually validate every redirect destination.
  8. 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.
  9. 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.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (5)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 61)May include surrounding context.

md
if (!submit.ok || !started.status_url) return { status: "error", message: `submit failed (${submit.status})` };

  // 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) {

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · kpn_call.mjs (reported line 40)May include surrounding context.

js
if (!submit.ok || !started.status_url) return { status: "error", message: `submit failed (${submit.status})` };

  // 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) {

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill instructs the agent to send the user's question and optional identifiers such as a wallet to an external service, but it does not explicitly warn that the content will leave the local agent boundary and be shared with a third party. This creates a privacy and consent risk, especially because business questions may contain confidential deal terms, partner names, strategy, or regulated information.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The optional notification flow collects webhook URLs, email addresses, and wallet identifiers for transmission to the third-party service without a clear warning about data sharing or retention. Contact details and callback endpoints are sensitive operational data, and exposing them without explicit consent can create privacy, spam, or infrastructure-targeting risks.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The top-level documentation says the code "does exactly what SKILL.md describes: submit a Korea question, poll for the human-verified verdict, return it." However, the exported function at L57-L63 sends contact details to a separate endpoint to register for future notifications, which is a distinct behavior not covered by that description. This is an active contradiction between the stated intent of the file and implemented functionality.

Content

No source excerpt is available for this finding.