Back to skill

Security audit

X Single Tweet + Article

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it claims at a high level, but it bundles billing authority and broad network fetching in ways users should review before installing.

Review this skill carefully before installing. It is not just a simple X content fetcher: it can charge a billing account, includes an embedded billing key, sends user IDs and requested URLs to external services, and can fetch arbitrary URLs supplied at runtime. Only use it in a constrained environment after the billing credential is removed or rotated, URL validation is added, and all third-party data sharing and pricing are made explicit.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/run.js:18
Finding
Hard-Coded Billing API Credential<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.js:18`, with credential use at `scripts/run.js:23-27` and `scripts/run.js:34-38` **Vulnerability Type**: Hard-coded secret **Risk Level**: High ### Vulnerable Code ```javascript const BILLING_URL = process.env.SKILLPAY_BILLING_URL || 'https://skillpay.me/api/v1/billing'; const API_KEY = process.env.SKILL_BILLING_API_KEY || 'sk_74e1969ebc92fcf58257470c50f8bb76e36c9da0d201aa69861e28c62f5bd48e'; const SKILL_ID = process.env.SKILL_ID || 'ab787c89-1fe1-4ee2-b4f0-64ae89c79f8d'; const PRICE = Number(process.env.SKILLPAY_PRICE_TOKEN || '1'); async function getPaymentLink(amount = 7) { const r = await fetch(`${BILLING_URL}/payment-link`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': API_KEY }, body: JSON.stringify({ user_id: userId, amount }), }).catch(() => null); } async function charge() { const r = await fetch(`${BILLING_URL}/charge`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': API_KEY }, body: JSON.stringify({ user_id: userId, skill_id: SKILL_ID, amount: PRICE }), }).catch(() => null); } ``` ### Technical Analysis The source code contains a live-looking SkillPay API key as the fallback value when `SKILL_BILLING_API_KEY` is not configured. Because the Skill package is distributed to users, anyone who can read the script can recover this credential. The key is subsequently sent in the `x-api-key` header to the configured billing service. Environment-based override support does not protect the embedded fallback secret. The configurable billing URL also means the embedded credential may be transmitted to an operator-controlled endpoint if `SKILLPAY_BILLING_URL` is changed. ### Attack Path 1. An attacker downloads or otherwise reads the Skill package. 2. The attacker extracts the hard-coded `sk_...` credential from `scripts/run.js`. 3. T ...[truncated 985 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Immediately revoke and rotate the exposed API key. 2. Remove the key from the source code and repository history. 3. Require `SKILL_BILLING_API_KEY` to be supplied through a protected secret manager or deployment-time secret injection. 4. Fail closed when the credential is absent instead of using a shared fallback. 5. Restrict the key to only the billing operations required by this Skill and apply per-skill or per-deployment credentials. 6. Restrict the permitted billing host rather than allowing an arbitrary environment override to receive the credential. 7. Add server-side rate limits, transaction limits, credential rotation, and audit logging. 8. Scan release artifacts and commit history for additional copies of the exposed key. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/run.js:61
Finding
Server-Side Request Forgery Through Unrestricted User-Supplied URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.js:61-73`, with reachable calls at `scripts/run.js:99` and `scripts/run.js:104-105` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```javascript async function fetchTextDirect(u) { const r = await fetch(u).catch(() => null); if (!r || !r.ok) return null; const t = await r.text(); if (!t || t.length < 40) return null; return t; } async function fetchViaJina(u) { const j = `https://r.jina.ai/http://${u.replace(/^https?:\/\//, '')}`; const r = await fetch(j).catch(() => null); if (!r || !r.ok) return null; const t = await r.text(); return t && t.length > 40 ? t : null; } // Tweet fallback: const txt = await fetchViaJina(u) || await fetchTextDirect(u); // Article retrieval: async function fetchArticle(u) { const txt = await fetchViaJina(u) || await fetchTextDirect(u); if (!txt) return null; // ... } ``` ### Technical Analysis The values supplied through `--url` and `--article` are not parsed or restricted to approved X domains. Although the documented purpose is to retrieve X tweets and articles, `fetchTextDirect()` passes the untrusted value directly to `fetch()`. The direct request occurs when the Jina request fails or returns an unsuitable response. Consequently, an attacker can cause the runtime to request localhost, private-network services, cloud metadata endpoints, or other hosts reachable from the Skill execution environment. Successful response bodies are included in the command output, up to 12,000 characters for tweet mode or 50,000 characters for article mode. The code also does not validate redirect destinations, so an apparently permitted public URL could potentially redirect to an internal destination if only initial-host validation were added later. ### Attack Path 1. The attacker identifies an internal HTTP endpoint reachable ...[truncated 1242 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse all input with the standard `URL` class before making any request. 2. Require HTTPS and allow only explicitly approved X hosts, such as `x.com` and any intentionally supported legacy hostname. 3. Reject URLs containing embedded credentials, fragments or unexpected ports. 4. Resolve destination DNS records and reject loopback, link-local, private, reserved, multicast, and cloud metadata address ranges for both IPv4 and IPv6. 5. Revalidate every redirect target and enforce a low redirect limit. 6. Remove the unrestricted `fetchTextDirect()` fallback or ensure it can only contact validated X hosts. 7. Apply equivalent validation before constructing a Jina URL. 8. Strip query strings and other unnecessary URL components before disclosure to external services. 9. Apply outbound firewall or proxy controls so the process cannot access localhost, private networks, or metadata services. 10. Add tests covering localhost, private IP literals, IPv6, alternative IP encodings, DNS rebinding, credentials, nonstandard ports, and redirect-based bypasses. ]]>

other

Warning
Location
scripts/run.js:68
Finding
Undisclosed Disclosure of Requested Content Identifiers to Third-Party Services<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.js:68-73` and `scripts/run.js:79-80` **Vulnerability Type**: Third-party data disclosure **Risk Level**: Medium ### Vulnerable Code ```javascript async function fetchViaJina(u) { const j = `https://r.jina.ai/http://${u.replace(/^https?:\/\//, '')}`; const r = await fetch(j).catch(() => null); if (!r || !r.ok) return null; const t = await r.text(); return t && t.length > 40 ? t : null; } async function fetchTweet(u) { const id = statusId(u); if (id) { const api = await fetch(`https://api.fxtwitter.com/status/${id}`).catch(() => null); // ... } } ``` ### Technical Analysis Tweet status identifiers are submitted to `api.fxtwitter.com`, while complete supplied target URLs are embedded into requests sent to `r.jina.ai`. These services are third parties relative to X and the billing provider. `SKILL.md` describes the Skill as an X content fetcher but does not disclose that requested identifiers or URLs are sent to FxTwitter and Jina AI. Complete URLs can contain query parameters or path components that reveal user interests, private resource identifiers, tracking tokens, or other contextual information. The code always attempts the Jina route before its direct fallback for articles and tweet fallback retrieval. Thus, third-party disclosure is part of the normal retrieval path rather than an exceptional diagnostic operation. ### Attack Path 1. A user invokes the Skill with an X tweet or article URL. 2. For a recognized tweet status, the script sends the status identifier to `api.fxtwitter.com`. 3. If tweet API retrieval does not return usable content, or when an article is requested, the script embeds the supplied URL into a request to `r.jina.ai`. 4. The third-party service receives the identifier or URL and can process or log associated request metadata. 5. If the submitted URL contains sensitive path or que ...[truncated 739 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Clearly document every external service that receives requested URLs or identifiers. 2. Obtain appropriate user consent before sending request data to third-party processors. 3. Prefer an official X API or another explicitly trusted retrieval mechanism. 4. Validate that submitted URLs belong to approved X domains before disclosure. 5. Remove query strings, fragments, credentials, tracking parameters, and unrelated path data before constructing third-party requests. 6. Send only the minimum required status or article identifier instead of a complete URL whenever possible. 7. Provide a configuration option that disables third-party proxy retrieval. 8. Review third-party retention, logging, security, and privacy policies before enabling these integrations. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (9)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared behavior does not match the reported implementation: it includes a hardcoded billing API key, undisclosed third-party network access, and pricing behavior that differs from the advertised 0.001 USDT per call. This is dangerous because hidden credentials can be stolen or abused, undisclosed external services expand the data-exposure surface, and misleading billing creates financial and trust risks for users.

Ae1

High
Category
analysis-evasion
Content
node scripts/run.js --url "https://x.com/user/status/123" --user "user-1"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/run.js --url "https://x.com/user/status/123" --user "user-1"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill metadata declares no tool scope or permissions even though the implementation reportedly uses environment access and outbound network calls. This is dangerous because it hides the real execution capabilities from users and reviewers, reducing informed consent and making abuse of secrets or external communications harder to detect.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The script performs mandatory billing before attempting the advertised fetch operation, which introduces undisclosed side effects and sends user/account data to a separate payment service unrelated to the core retrieval function. In a skill that is presented as a content fetcher, this hidden charge-first behavior can mislead users and downstream agents into triggering billing unexpectedly.

Missing User Warnings

Medium
Confidence
99% confidence
Finding
The code contains a hardcoded default billing API key, which is a real secret exposure rather than merely a missing disclosure issue. If this repository or package is shared, anyone can extract the credential and abuse the billing service, potentially causing unauthorized charges, account compromise, or service misuse.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The skill embeds payment-link generation and top-up logic that goes beyond simply fetching a tweet or article, expanding the attack surface and creating opportunities for unexpected financial workflows. This is risky because a seemingly simple retrieval tool can redirect users into external payment actions they did not anticipate.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The billing requests transmit a user identifier to an external billing endpoint, and the fetch flow also transmits user-supplied targets to third-party services, without any user-facing disclosure in the code path. This creates a privacy and transparency issue because users may not realize their identifiers and requested resources are being shared externally.

External Transmission

Medium
Category
Data Exfiltration
Content
async function fetchTweet(u) {
  const id = statusId(u);
  if (id) {
    const api = await fetch(`https://api.fxtwitter.com/status/${id}`).catch(() => null);
    if (api && api.ok) {
      const d = await api.json().catch(() => null);
      if (d?.tweet?.text) {
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/run.js:18