Back to skill

Security audit

Turnstile Spin

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its Turnstile setup purpose, but it asks for high-impact Cloudflare access while relying on mutable remote instructions and deploy/install payloads that need human review.

Install only after reviewing the scripts and being comfortable with Cloudflare account mutations. Use a short-lived, account-scoped token, avoid pasting tokens into chat, rotate any token or Turnstile secret that appears in logs, prefer the bundled Worker template over remote downloads, and do not run the persistence step unless the source is pinned or otherwise verified.

Vulnerability Patterns
  • 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
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
Findings (6)

T01 · Skill Instruction Hijacking

Error
Location
SKILL.md:17
Finding

Remote Documentation Can Override Audited Skill Instructions

Content
View full analysis
Remediation
View remediation

T03 · Remote Payload Retrieval and Execution

Error
Location
scripts/worker-deploy.sh:35
Finding

Mutable Remote Worker Template Is Downloaded and Deployed with Cloudflare Credentials

Content
View full analysis
/dev/null 2>&1 # Capture both streams. Wrangler prints the success URL and version ID on # stdout; progress indicators on stderr. Capturing only stderr loses the URL. (cd "$DEPLOY_DIR" && npx wrangler deploy --name "$target_name") >"$deploy_log" 2>&1 } ``` ### Technical Analysis The project includes an auditable Worker under `templates/worker`, but the deployment script does not deploy that local copy. Instead, it invokes an unpinned `npx --yes degit` command to retrieve the current contents of a remote GitHub repository. It then invokes Wrangler from the downloaded directory. Neither the `degit` package nor the repository payload is pinned to an immutable version or commit, and no cryptographic checksum is verified. The effective deployed source can therefore change after this Skill has been reviewed. Wrangler's build and deploy process handles the downloaded project while `CLOUDFLARE_API_TOKEN` is present in the environment. ### Attack Path 1. The user authorizes Turnstile setup and supplies a Cloudflare API token. 2. An attacker compromises the remote repository, package-resolution path, or an unpinned dependency used by `npx`. 3. The attacker modifies the remote Worker template or package configuration. 4. `worker-deploy.sh` downloads the modified payload without checking a commit or digest. 5. Wrangler processes and deploys the payload using the user's Cloudflare credentials. 6. The malicious Worker becomes publicly accessible in the user's Cloudflare account and may process future Turnstile tokens or other submitted data. ### Impact Assessment An at ...[truncated 573 chars]
Remediation
View remediation

T02 · Agent Memory Poisoning

Error
Location
scripts/persist-skill.sh:30
Finding

Persistent Agent Skill Is Installed from Unpinned Remote Content

Content
View full analysis
/dev/null 2>&1; then echo "persist-skill: degit failed; cannot fetch cloudflare/skills/skills/turnstile-spin." >&2 echo "persist-skill: ensure your network can reach github.com and try again, or install manually." >&2 echo "{\"status\":\"error\",\"reason\":\"degit_failed\"}" exit 1 fi if [ ! -f "$TARGET_DIR/SKILL.md" ]; then echo "persist-skill: bundle extracted but SKILL.md is missing at $TARGET_DIR/SKILL.md." >&2 echo "{\"status\":\"error\",\"reason\":\"skill_missing\"}" exit 1 fi # Make scripts executable so the agent can invoke them directly. if [ -d "$TARGET_DIR/scripts" ]; then chmod +x "$TARGET_DIR/scripts"/*.sh 2>/dev/null || true fi ``` ### Technical Analysis The persistence operation does not copy the currently audited local Skill. It downloads a mutable remote bundle into an Agent Skill directory such as `.claude/skills/turnstile-spin`, verifies only that `SKILL.md` exists, and marks downloaded shell scripts executable. Agent Skill directories can affect later sessions. Consequently, a remote compromise or post-review change can install attacker-controlled instructions and executable scripts into persistent Agent state. The unpinned `npx --yes degit` invocation adds a second mutable supply-chain dependency. ### Attack Path 1. The user agrees to save the Skill for reuse. 2. An attacker has modified the remote repository or compromised package resolution for `degit`. 3. `persist-skill.sh` downloads the altered bundle directly into the Agent's persistent Skill directory. 4. The sc ...[truncated 704 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/worker-deploy.sh:25
Finding

Caller-Controlled Deployment Directory Is Recursively Deleted Without Safety Validation

Content
View full analysis
&2; exit 2 ;; esac done : "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN must be set}" : "${WIDGET_SECRET:?WIDGET_SECRET must be set}" deploy_log=$(mktemp) deploy() { local target_name="$1" rm -rf "$DEPLOY_DIR" ``` ### Technical Analysis The `--deploy-dir` argument is accepted from the caller and passed directly to `rm -rf`. Shell quoting prevents word splitting and wildcard expansion, but it does not make the selected path safe. There is no canonicalization, temporary-directory prefix check, symlink check, or rejection of dangerous locations such as `/`, the home directory, or the project root. An Agent instruction error, malicious prompt content that influences command construction, or direct invocation can therefore convert a deployment operation into arbitrary directory deletion under the current user's permissions. ### Attack Path 1. An attacker influences the invocation or a user mistakenly supplies a valuable path through `--deploy-dir`. 2. The script assigns that path to `DEPLOY_DIR` without validation. 3. The `deploy` function runs `rm -rf "$DEPLOY_DIR"`. 4. Files beneath the selected directory are recursively removed before any download or deployment occurs. 5. The later retrieval step may recreate the directory, obscuring the original deletion. ### Impact Assessment The process can delete any directory writable by the invoking user. This may include the current project, source repositories, local configuration, caches, or Agent Skill directories. If invoked by a more pr ...[truncated 170 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/widget-create.sh:42
Finding

Turnstile Widget Secrets Are Exposed in Standard Output

Content
View full analysis
/dev/null || python3 -c "import sys,json; print(str(json.load(sys.stdin).get('success',False)).lower())")) if [ "$success" = "true" ]; then sitekey=$(echo "$body" | (jq -r '.result.sitekey' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result']['sitekey'])")) secret=$(echo "$body" | (jq -r '.result.secret' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result']['secret'])")) echo "{\"status\":\"ok\",\"sitekey\":\"$sitekey\",\"secret\":\"$secret\"}" exit 0 fi ``` A corresponding recovery-flow disclosure exists at `scripts/fetch-secret.sh:44-50`: ```sh if [ "$http_code" = "200" ]; then secret=$(echo "$body" | (jq -r '.result.secret' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result']['secret'])")) clearance=$(echo "$body" | (jq -r '.result.clearance_level // "no_clearance"' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result'].get('clearance_level','no_clearance'))")) domains=$(echo "$body" | (jq -c '.result.domains // []' 2>/dev/null || python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin)['result'].get('domains',[])))")) if [ -n "$secret" ] && [ "$secret" != "null" ]; then echo "{\"status\":\"ok\",\"secret\":\"$secret\",\"clearance_level\":\"$clearance\",\"domains\":$domains}" exit 0 fi fi ``` ### Technical Analysis Both scripts serialize a private widget secret into stdout. In an Agent workflow, stdout is commonly captured by tool results, orchestration logs, terminal scrollback, CI systems, or conversation history. This conflicts with the Skill's stated goal that the secret should remain only in the environment and be passed to Wrangler through standard input. ...[truncated 1084 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/auth-probe.sh:33
Finding

Authentication Probe Encourages Broader Read Access and Enumerates Token-Accessible Accounts

Content
View full analysis
/dev/null || true) if [ -z "$whoami_json" ] || [ "$(echo "$whoami_json" | head -c 1)" != "{" ]; then echo "auth-probe: wrangler whoami returned no JSON. Token may be invalid or expired." >&2 emit '{"status":"missing_token","reason":"whoami_failed"}' fi # Extract the accounts array. Fall back to python3 if jq is missing. accounts_json=$(echo "$whoami_json" | (jq -c '.accounts' 2>/dev/null || python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin)['accounts']))")) account_count=$(echo "$accounts_json" | (jq 'length' 2>/dev/null || python3 -c "import sys,json; print(len(json.load(sys.stdin)))")) ``` ```sh # Probe Workers scope on the selected account. GET /workers/scripts requires # Account.Workers Scripts:Read, which is a best-effort proxy for Edit. Tokens # granted Edit-only (without Read) will fail this probe and emit a confusing # missing_workers_scope; the agent should suggest adding Read alongside Edit. tmp=$(mktemp) workers_code=$(curl -sS -w "%{http_code}" -o "$tmp" \ "https://api.cloudflare.com/client/v4/accounts/$account_id/workers/scripts" \ -H "Authorization: Bearer $token" 2>/dev/null || echo "000") workers_body=$(cat "$tmp"); rm -f "$tmp" workers_success=$(echo "$workers_body" | (jq -r '.success' 2>/dev/null || echo "false")) if [ "$workers_success" != "true" ]; then echo "auth-probe: token cannot read /workers/scripts on account $account_id (HTTP $workers_code). Missing Account.Workers Scripts:Edit." >&2 emit "{\"status\":\"missing_workers_scope\",\"account_id\":\"$account_id\",\"http_code\":$workers_code}" fi ``` ### Technical Analysis The Skill legitimately needs permission to create a Turnstile widget and deploy a Worker. However, the probe uses acc ...[truncated 1623 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (67)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

This variant highlights that the skill requires Cloudflare API token and account access while declaring no permissions. That mismatch is security-relevant because it can cause users or orchestrators to trust the skill as documentation-only while it actually drives privileged remote actions using credentials.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding

This variant highlights that the skill requires Cloudflare API token and account access while declaring no permissions. That mismatch is security-relevant because it can cause users or orchestrators to trust the skill as documentation-only while it actually drives privileged remote actions using credentials.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

This variant highlights that the skill requires Cloudflare API token and account access while declaring no permissions. That mismatch is security-relevant because it can cause users or orchestrators to trust the skill as documentation-only while it actually drives privileged remote actions using credentials.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

This variant highlights that the skill requires Cloudflare API token and account access while declaring no permissions. That mismatch is security-relevant because it can cause users or orchestrators to trust the skill as documentation-only while it actually drives privileged remote actions using credentials.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
89% confidence
Finding

This variant highlights that the skill requires Cloudflare API token and account access while declaring no permissions. That mismatch is security-relevant because it can cause users or orchestrators to trust the skill as documentation-only while it actually drives privileged remote actions using credentials.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

This variant highlights that the skill requires Cloudflare API token and account access while declaring no permissions. That mismatch is security-relevant because it can cause users or orchestrators to trust the skill as documentation-only while it actually drives privileged remote actions using credentials.

Content

No source excerpt is available for this finding.

Chaining Abuse

High
Category
Tool Misuse
Confidence
75% confidence
Finding

Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Content

Scanner excerpt · scripts/auth-probe.sh (reported line 72)May include surrounding context.

sh
http_code=$(curl -sS -w "%{http_code}" -o "$tmp" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/challenges/widgets/$SITEKEY" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" 2>/dev/null || echo "000")
body=$(cat "$tmp"); rm -f "$tmp"

if [ "$http_code" = "200" ]; then
  secret=$(echo "$body" | (jq -r '.result.secret' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result']['secret'])"))

Chaining Abuse

High
Category
Tool Misuse
Confidence
75% confidence
Finding

Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Content

Scanner excerpt · scripts/auth-probe.sh (reported line 88)May include surrounding context.

sh
http_code=$(curl -sS -w "%{http_code}" -o "$tmp" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/challenges/widgets/$SITEKEY" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" 2>/dev/null || echo "000")
body=$(cat "$tmp"); rm -f "$tmp"

if [ "$http_code" = "200" ]; then
  secret=$(echo "$body" | (jq -r '.result.secret' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result']['secret'])"))

Chaining Abuse

High
Category
Tool Misuse
Confidence
75% confidence
Finding

Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Content

Scanner excerpt · scripts/fetch-secret.sh (reported line 42)May include surrounding context.

sh
http_code=$(curl -sS -w "%{http_code}" -o "$tmp" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/challenges/widgets/$SITEKEY" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" 2>/dev/null || echo "000")
body=$(cat "$tmp"); rm -f "$tmp"

if [ "$http_code" = "200" ]; then
  secret=$(echo "$body" | (jq -r '.result.secret' 2>/dev/null || python3 -c "import sys,json; print(json.load(sys.stdin)['result']['secret'])"))

Skill Enumeration

Medium
Category
Agent Snooping
Confidence
85% confidence
Finding

Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.

Content

Scanner excerpt · README.md (reported line 26)May include surrounding context.

sh
# Claude Code
mkdir -p .claude/skills/turnstile-spin && \
  curl -sSL https://developers.cloudflare.com/turnstile/spin.md \
  -o .claude/skills/turnstile-spin/SKILL.md

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · README.md (reported line 26)May include surrounding context.

sh
# Claude Code
mkdir -p .claude/skills/turnstile-spin && \
  curl -sSL https://developers.cloudflare.com/turnstile/spin.md \
  -o .claude/skills/turnstile-spin/SKILL.md

Skill Enumeration

Medium
Category
Agent Snooping
Confidence
85% confidence
Finding

Skill enumerates or reads other installed skills. Access to other skills' SKILL.md files or the skills directory reveals prompt instructions, capabilities, and secrets that should be invisible to peer skills.

Content

Scanner excerpt · README.md (reported line 31)May include surrounding context.

-o .claude/skills/turnstile-spin/SKILL.md

Or, install the whole skills bundle into a global location

git clone https://github.com/cloudflare/skills ~/.config/cloudflare-skills ln -s ~/.config/cloudflare-skills/turnstile-spin ~/.claude/skills/turnstile-spin

text

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill orchestrates shell commands, environment-variable access, and network/API actions against Cloudflare, but it declares no explicit tool scope or permission boundary. In an agent setting, that increases the chance the skill will be invoked with broader capabilities than intended, enabling unexpected code execution or credential use without clear user-visible constraints.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
69% confidence
Finding

The skill is designed to persist itself for reuse, which creates session-to-session state and can extend the lifetime of a privileged workflow in future tasks. In isolation this is not malicious, but persistence of an automation skill that handles credentials and remote deployment increases the blast radius if later invoked unexpectedly or in a different context.

Content

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

md
---
name: turnstile-spin
description: Set up Cloudflare Turnstile end-to-end in a project — scan the codebase, create the widget via the Cloudflare API, deploy the managed siteverify Worker, write the frontend snippets, validate, and persist the skill. Load this when a user asks to add Turnstile, set up CAPTCHA, protect a form from bots, or fix a Turnstile integration. Mirrors developers.cloudflare.com/turnstile/spin.
references:
  - vanilla-html
  - nextjs-app

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

The trigger phrases are broad enough to load the skill for generic anti-spam or form-protection requests, after which it can begin credential probing, project scanning, and remote deployment orchestration. In agent ecosystems, over-broad auto-activation increases the risk of unintended privileged actions in contexts where the user did not specifically request Cloudflare Turnstile setup.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding

Using npx wrangler --version without pinning a version allows resolution of whatever package version is current in the environment or registry at execution time. This creates supply-chain and reproducibility risk, because behavior may change unexpectedly or a compromised package version could be pulled during a privileged workflow.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The skill explicitly offers users the option to paste a Cloudflare API token into chat, but the overview does not foreground the sensitivity of that credential or the risks of conversation-log retention. This can lead to long-lived account tokens being exposed in transcripts, shared logs, or downstream analytics, enabling account compromise and remote resource manipulation.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding

The skill instructs use of npx wrangler secret put without version pinning during secret handling and deployment. Because this command operates in a privileged deployment path, an unexpected or malicious package version could affect how secrets are processed or where they are sent.

Content

No source excerpt is available for this finding.

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
75% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

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

md
- Do not overwrite files without showing a diff.
- Do not deploy a Worker to a different account than the widget was created in.
- Do not call siteverify from the browser. Always: browser → user's Worker → siteverify.
- Do not use `sudo` or install global packages without asking.

### Hard scope boundary: DO NOT ask the user about

Autonomous Decision Making

Medium
Category
Excessive Agency
Confidence
80% confidence
Finding

Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Content

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

md
- Do not call siteverify from the browser. Always: browser → user's Worker → siteverify.
- Do not use `sudo` or install global packages without asking.

### Hard scope boundary: DO NOT ask the user about

Spin validates the Turnstile token via a managed Worker before the user's existing form handler runs. Everything else is out of scope:

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

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

md
| `wrangler` not installed | Install path: `npm install --save-dev wrangler` (Node project) or `npm install -g wrangler` (other) |
| Multiple Cloudflare accounts | `scripts/auth-probe.sh` returns all accounts; ask the user to choose, export `CLOUDFLARE_ACCOUNT_ID` |
| Cloudflare Pages project | Deploy the managed Worker anyway, OR suggest the [Pages Plugin](https://developers.cloudflare.com/pages/functions/plugins/turnstile/) |
| `EXPECTED_HOSTNAME` mismatch | Update widget domains via PUT, not PATCH (PATCH returns `10405 Method not allowed`): `curl -X PUT .../widgets/$SITEKEY -d '{"name":"...","mode":"managed","domains":[...]}'` |
| Worker name conflict | `worker-deploy.sh` retries automatically with a hash suffix |
| Token expired mid-flow | Stop, re-run `scripts/auth-probe.sh`, prompt for fresh credentials |
| Step 11 returns `missing-input-secret` | Secret didn't propagate. Re-set: `echo "$WIDGET_SECRET" \| npx wrangler secret put TURNSTILE_SECRET_KEY --name <worker_name from worker-deploy.sh output>`, wait 10s, re-validate. Use the `worker_name` field returned by `worker-deploy.sh`; do not rely on a `$WORKER_NAME` env var. |

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding

This unpinned npx wrangler secret put invocation appears in an error-recovery path, which may be executed under operational pressure and with sensitive material in scope. Unpinned package execution in such paths increases supply-chain exposure and can undermine consistent secret-management behavior.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding

The second recovery instruction also relies on unpinned npx wrangler, repeating the same supply-chain risk in another secret-setting flow. Repetition across multiple instructions increases the chance that operators will execute insecure package resolution in production contexts.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The file states that the form posts directly to the Worker, and the example form includes an email field submitted to that endpoint. The markdown does not warn users that entered email data will be transmitted to a remote service, which is a privacy-relevant behavior for a skill description.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

This markdown file includes a Cloudflare Turnstile script and a subscription form that posts the user's email address to a worker URL, but the surrounding instructions do not warn users about third-party requests or transmission of user data. For markdown files, SQP-2 applies when the skill description omits warnings about behaviors that could affect user data or privacy.

Content

No source excerpt is available for this finding.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
templates/worker/test/deploy.test.ts:49