Edge Security Lab
Two live Workers demos built for a fintech customer call: Turnstile Ephemeral IDs for fraud detection in mobile WebViews, and a three-tier security deception system that serves fake data to bots instead of blocking them. Both run on Cloudflare Workers and demonstrate application security patterns relevant to banking.
The Problem
A fintech customer serving hundreds of credit unions needed help with two security challenges: fraudulent account creation through their mobile banking app, and API scraping by automated tools. Their security team wanted to see practical implementations, not slide decks.
For fraud, the challenge is that attackers rotate through residential proxy pools — hundreds of unique IPs creating accounts. Traditional rate-limiting-by-IP is useless. They needed a way to fingerprint the underlying device without cookies or localStorage.
For API security, the conventional approach — returning 403 to detected bots — tells the attacker exactly when they've been caught. The customer wanted a "security through obscurity" approach: keep attackers guessing by serving plausible fake data instead of blocking them.
What I Built
Demo 1: Turnstile + Ephemeral IDs. A credit union member signup form protected by Cloudflare Turnstile in invisible mode. On submission, the Worker calls Cloudflare's siteverify API and displays the full response — including the Ephemeral ID, a short-lived device fingerprint derived from browser signals without cookies. A companion fraud dashboard shows how multiple signups from the same device cluster under one Ephemeral ID, even across different IPs. After 4+ signups, the dashboard flags the device as suspicious.
Demo 2: Security Through Obscurity. A simulated banking API with three response tiers: real data for humans, poisoned data for suspected bots (real structure, corrupted values), and completely fabricated data for confirmed bots — all returning HTTP 200. The demo includes honeypot trap endpoints (/.env, /admin/members.json) that serve convincing fake credentials and member records, plus a tarpit that slow-drips JSON over 30 seconds to waste bot connections. An interactive dashboard lets you hit the same endpoint as a human, a suspected bot, or a confirmed bot and compare the responses side-by-side.
Architecture
Demo 1: Turnstile Fraud Detection
──────────────────────────────────
Browser (WebView)
└── Signup form with Turnstile widget (invisible)
│ cf-turnstile-response token
▼
Worker (turnstile-demo.macksportreport.com)
├── POST /api/signup
│ ├── Calls challenges.cloudflare.com/turnstile/v0/siteverify
│ ├── Extracts Ephemeral ID from metadata
│ ├── Logs event to in-memory fraud store
│ ├── Velocity check: >3 signups/hour from same device?
│ └── Returns full siteverify response + fraud analysis
│
└── GET /dashboard
└── Aggregates events by Ephemeral ID
└── Flags devices with >3 signups as SUSPICIOUS
Demo 2: Security Deception
──────────────────────────
Incoming Request
│
▼
Worker (honeypot-demo.macksportreport.com)
├── Is path a honeypot? (/.env, /admin/*)
│ └── YES → Log trap hit → Serve fake credentials (200)
│
├── Bot detection (ASN, UA, TLS fingerprint, Accept-Language)
│ ├── Score = DEFINITE BOT → Completely fake JSON (200)
│ ├── Score = SUSPICIOUS → Poisoned real data (200)
│ └── Score = HUMAN → Real data (200)
│
└── /api/tarpit → Streaming slow-drip (2s per record, 30s total)
└── Ties up bot connection pool Technical Deep Dive
Ephemeral IDs — Device Fingerprinting Without Cookies
Turnstile's Ephemeral IDs are short-lived device identifiers derived server-side from aggregated browser signals — proof-of-work completion speed, JS engine behavior, Web API surface (which APIs exist), canvas/WebGL rendering, navigator properties, and behavioral timing. No cookies, no localStorage, no sessionStorage. The ID appears only in the siteverify API response — the browser never sees it. This makes it invisible to attackers and impossible to spoof. The catch: privacy-focused devices (iPhones, Safari) limit observable signals, so multiple legitimate users may share the same ID. Fraud detection must use threshold-based velocity logic, not uniqueness.
Five-Signal Fraud Scoring Engine
Each request is scored across five signal layers, each contributing 0–3 points (max 15). The response tier is determined by the combined score — not any single signal. This is intentional: an attacker can defeat any one signal at scale, but defeating all five simultaneously becomes economically impossible.
- • Ephemeral ID velocity — how many times has this device acted in the last hour? 1 = clean, 7+ = automated.
- • Bot Management score — Cloudflare's ML model (1–99) based on mouse movement, click timing, JS engine behavior, headless browser detection. Computed before the Worker runs.
- • TLS fingerprint (JA4) — what software is actually making this request? python-requests claiming to be Chrome 126 is caught by the JA4 mismatch.
- • Request signals — ASN (DigitalOcean = bot farm), geo mismatch (Nigeria + Michigan credit union), missing Accept-Language header.
- • Application signals — form completed in 0.4 seconds, disposable email domain. Business logic only Bankjoy can define.
Three-Tier Response System + Poisoned Data
The Worker never returns 403 — that reveals detection. Instead, score 0–5 gets real data, score 6–9 gets poisoned data (real schema, corrupted values: emails with random suffixes, randomized balances, fabricated routing numbers), and score 10–15 gets completely fake data (fabricated records, random UUIDs, fake email domains). Poisoned data is sneakier than fake data: the attacker gets the same record count, the same IDs, the same structure — just subtly wrong values. To detect it, they'd need a legitimate account to compare against, and even then it's indistinguishable from normal API caching behavior.
Order of Operations
Cloudflare pre-computes bot score, JA4 fingerprint, ASN, geo, and TLS version during the connection handshake — before any Worker code runs. By the time the Worker executes, those signals are already on request.cf. The Worker then calls Turnstile siteverify (~50–100ms), checks the KV blocklist (sub-millisecond), queries D1 for velocity (~5–10ms), scores all five signals, and returns the appropriate response tier. Total added latency: ~113ms. The logging, blocklist write, and Slack alert all happen in ctx.waitUntil() after the response is sent.
Tarpit Pattern
The tarpit uses Workers' streaming response support (TransformStream) to hold a connection open for ~30 seconds, dripping one JSON record every 2 seconds. This isn't about wasting bandwidth — it's about wasting the attacker's connection pool. A bot running 100 concurrent connections has all 100 tied up for 30 seconds each, reducing effective throughput by 30x. Uses wall-clock time (setTimeout), not CPU time, staying within Workers' CPU limits.
Live Endpoints
Turnstile Demo
- turnstile-demo.macksportreport.com — Signup form
- turnstile-demo.macksportreport.com/dashboard — Fraud dashboard
Honeypot Demo
- honeypot-demo.macksportreport.com — Interactive dashboard
- honeypot-demo.macksportreport.com/admin/members.json — Honeypot trap
Cloudflare Products Used
Workers — Both demos run entirely on Workers. The Turnstile demo handles form submission, calls siteverify, logs fraud events, and serves the dashboard. The honeypot demo implements bot detection, three-tier response generation, canary endpoints, and streaming tarpits — all in a single Worker.
Turnstile — Invisible challenge widget on the signup form. Test keys simulate the flow; Enterprise keys would enable real Ephemeral IDs. The siteverify API returns the device fingerprint, challenge timestamp, action label, and hostname for server-side validation.
KV — Stores honeypot trap events with 30-day TTL. In production, this would power a blocklist: any IP hitting a canary endpoint gets its traffic permanently routed to fake data.
Custom Domains — Both Workers are deployed to subdomains of macksportreport.com using Workers Custom Domains, giving them clean demo URLs and automatic SSL.
What I Learned
- • Never return 403 to a detected bot. A 403 tells the attacker "I caught you." A 200 with fake data tells them nothing — they think they succeeded while their scraped database fills with garbage.
- • Ephemeral IDs are a similarity signal, not a unique identifier. Multiple legitimate users on iPhones may share the same ID. Design fraud detection around thresholds (3+ signups per ID per hour), not uniqueness.
- • WebView integration is fragile. DOM storage must be enabled, User-Agent must stay consistent, cookies must persist, and the page must be served over HTTP/HTTPS (not file://). Missing any one causes silent failure.
- • Canary endpoints have zero false-positive risk. No legitimate user ever requests /.env or /admin/members.json. Any client hitting these paths is definitively a scanner — no ML scoring needed.
- • Poisoned data is sneakier than fake data. Real schema structure with corrupted values (wrong emails, randomized balances) is harder for attackers to detect than entirely fabricated records.
- • Cloudflare's own WAF catches dotfile access. When deploying the honeypot behind Cloudflare's proxy, the managed WAF blocked requests to /.env before the Worker could serve fake credentials. Defense in depth — the WAF handles the obvious attacks, the Worker handles the sophisticated ones.
Why I Built This
- • Customer calls land better with live demos than slides. A security VP watching fake SSNs stream out of a honeypot endpoint understands the value immediately — no architecture diagrams needed.
- • Building the demos forced me to hit every edge case before the customer did: WebView gotchas, token expiration in long forms, tarpit CPU limits, and the WAF catching honeypot paths.
- • Both patterns are reusable. The Turnstile demo works for any customer evaluating fraud detection, and the honeypot demo works for any customer with API scraping concerns. The code is portable — just change the fake data shapes to match the customer's domain.