T01 · Skill Instruction Hijacking
- Location
SKILL.md:17- Finding
Remote Documentation Can Override Audited Skill Instructions
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
SKILL.md:17Remote Documentation Can Override Audited Skill Instructions
scripts/worker-deploy.sh:35Mutable Remote Worker Template Is Downloaded and Deployed with Cloudflare Credentials
scripts/persist-skill.sh:30Persistent Agent Skill Is Installed from Unpinned Remote Content
scripts/worker-deploy.sh:25Caller-Controlled Deployment Directory Is Recursively Deleted Without Safety Validation
scripts/widget-create.sh:42Turnstile Widget Secrets Are Exposed in Standard Output
scripts/auth-probe.sh:33Authentication Probe Encourages Broader Read Access and Enumerates Token-Accessible Accounts
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.
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.
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.
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.
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.
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.
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.
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'])"))
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.
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'])"))
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.
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 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.
# 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 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.
# 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 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.
-o .claude/skills/turnstile-spin/SKILL.md
git clone https://github.com/cloudflare/skills ~/.config/cloudflare-skills ln -s ~/.config/cloudflare-skills/turnstile-spin ~/.claude/skills/turnstile-spin
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.
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.
---
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
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.
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.
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.
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.
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.
- 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
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.
- 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:
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
| `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. |
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.
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.
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.
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.
Detected: suspicious.dangerous_exec