Skip to content
Back to skills

1570 Gotchas B8081dd6

ASecurity

**Problem:** Client-only validation is easily bypassed.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
developmentjavascriptgojavabashreactvuenextjsnodedebuggingapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 1570-gotchas_b8081dd6 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 1570 Gotchas B8081dd6?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 1570 Gotchas B8081dd6
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-1570-gotchas-b8081dd6/badge)](https://www.skillsdirectory.com/skills/tools-only-1570-gotchas-b8081dd6)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
# Troubleshooting & Gotchas

## Critical Rules

### ❌ Skipping Server-Side Validation

**Problem:** Client-only validation is easily bypassed.

**Solution:** Always validate on server.

```javascript
// CORRECT - Server validates token
app.post('/submit', async (req, res) => {
  const token = req.body['cf-turnstile-response']
  const validation = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    body: JSON.stringify({ secret: SECRET, response: token }),
  }).then((r) => r.json())

  if (!validation.success) return res.status(403).json({ error: 'CAPTCHA failed' })
})
```

### ❌ Exposing Secret Key

**Problem:** Secret key leaked in client-side code.

**Solution:** Server-side validation only. Never send secret to client.

### ❌ Reusing Tokens (Single-Use Rule)

**Problem:** Tokens are single-use. Revalidation fails with `timeout-or-duplicate`.

**Solution:** Generate new token for each submission. Reset widget on error.

```javascript
if (!response.ok) window.turnstile.reset(widgetId)
```

### ❌ Not Handling Token Expiry

**Problem:** Tokens expire after 5 minutes.

**Solution:** Handle expiry callback or use auto-refresh.

```javascript
window.turnstile.render('#container', {
  sitekey: 'YOUR_SITE_KEY',
  'refresh-expired': 'auto', // or 'manual' with expired-callback
  'expired-callback': () => window.turnstile.reset(widgetId),
})
```

## Common Errors

| Error                      | Cause                                             | Solution                                                          |
| -------------------------- | ------------------------------------------------- | ----------------------------------------------------------------- |
| **Widget not rendering**   | Incorrect sitekey, CSP blocking, file:// protocol | Check sitekey, add CSP for challenges.cloudflare.com, use http:// |
| **timeout-or-duplicate**   | Token expired (>5min) or reused                   | Generate fresh token, don't cache >5min                           |
| **invalid-input-secret**   | Wrong secret key                                  | Verify secret from dashboard, check env vars                      |
| **missing-input-response** | Token not sent                                    | Check form field name is 'cf-turnstile-response'                  |

## Framework Gotchas

### React: Widget Re-mounting

**Problem:** Widget re-renders on state change, losing token.

**Solution:** Control lifecycle with useRef.

```tsx
function TurnstileWidget({ onToken }) {
  const containerRef = useRef(null)
  const widgetIdRef = useRef(null)

  useEffect(() => {
    if (containerRef.current && !widgetIdRef.current) {
      widgetIdRef.current = window.turnstile.render(containerRef.current, {
        sitekey: 'YOUR_SITE_KEY',
        callback: onToken,
      })
    }
    return () => {
      if (widgetIdRef.current) {
        window.turnstile.remove(widgetIdRef.current)
        widgetIdRef.current = null
      }
    }
  }, [])

  return <div ref={containerRef} />
}
```

### React StrictMode: Double Render

**Problem:** Widget renders twice in dev due to StrictMode.

**Solution:** Use cleanup function.

```tsx
useEffect(() => {
  const widgetId = window.turnstile.render('#container', { sitekey })
  return () => window.turnstile.remove(widgetId)
}, [])
```

### Next.js: SSR Hydration

**Problem:** `window.turnstile` undefined during SSR.

**Solution:** Use `'use client'` or dynamic import with `ssr: false`.

```tsx
'use client'
export default function Turnstile() {
  /* component */
}
```

### SPA: Navigation Without Cleanup

**Problem:** Navigating leaves orphaned widgets.

**Solution:** Remove widget in cleanup.

```javascript
// Vue
onBeforeUnmount(() => window.turnstile.remove(widgetId))

// React
useEffect(() => () => window.turnstile.remove(widgetId), [])
```

## Network & Security

### CSP Blocking

**Problem:** Content Security Policy blocks script/iframe.

**Solution:** Add CSP directives.

```html
<meta
  http-equiv="Content-Security-Policy"
  content="script-src 'self' https://challenges.cloudflare.com; 
               frame-src https://challenges.cloudflare.com;"
/>
```

### IP Address Forwarding

**Problem:** Server receives proxy IP instead of client IP.

**Solution:** Use correct header.

```javascript
// Cloudflare Workers
const ip = request.headers.get('CF-Connecting-IP')

// Behind proxy
const ip = request.headers.get('X-Forwarded-For')?.split(',')[0]
```

### CORS (Siteverify)

**Problem:** CORS error calling siteverify from browser.

**Solution:** Never call siteverify client-side. Call your backend, backend calls siteverify.

## Limits & Constraints

| Limit          | Value                                  | Impact                       |
| -------------- | -------------------------------------- | ---------------------------- |
| Token validity | 5 minutes                              | Must regenerate after expiry |
| Token use      | Single-use                             | Cannot revalidate same token |
| Widget size    | 300x65px (normal), 130x120px (compact) | Plan layout                  |

## Debugging

### Console Logging

```javascript
window.turnstile.render('#container', {
  sitekey: 'YOUR_SITE_KEY',
  callback: (token) => console.log('✓ Token:', token),
  'error-callback': (code) => console.error('✗ Error:', code),
  'expired-callback': () => console.warn('⏱ Expired'),
  'timeout-callback': () => console.warn('⏱ Timeout'),
})
```

### Check Token State

```javascript
const token = window.turnstile.getResponse(widgetId)
console.log('Token:', token || 'NOT READY')
console.log('Expired:', window.turnstile.isExpired(widgetId))
```

### Test Keys (Use First)

Always develop with test keys before production:

- Site: `1x00000000000000000000AA`
- Secret: `1x0000000000000000000000000000000AA`

### Network Tab

- Verify `api.js` loads (200 OK)
- Check siteverify request/response
- Look for 4xx/5xx errors

## Misconfigurations

### Wrong Key Pairing

**Problem:** Site key from one widget, secret from another.

**Solution:** Verify site key and secret are from same widget in dashboard.

### Test Keys in Production

**Problem:** Using test keys in production.

**Solution:** Environment-based keys.

```javascript
const SITE_KEY = process.env.NODE_ENV === 'production' ? process.env.TURNSTILE_SITE_KEY : '1x00000000000000000000AA'
```

### Missing Environment Variables

**Problem:** Secret undefined on server.

**Solution:** Check .env and verify loading.

```bash
# .env
TURNSTILE_SECRET=your_secret_here

# Verify
console.log('Secret loaded:', !!process.env.TURNSTILE_SECRET);
```

## Reference

- [Turnstile Docs](https://developers.cloudflare.com/turnstile/)
- [Dashboard](https://dash.cloudflare.com/?to=/:account/turnstile)
- [Error Codes](https://developers.cloudflare.com/turnstile/troubleshooting/)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.