Skip to content

Security: Add rate limiting to email tracking Cloudflare Worker #294

@joshuayoes

Description

@joshuayoes

Summary

The email tracking pixel endpoint (/p/:blob.gif) has no rate limiting, allowing enumeration attacks and resource abuse. The bot detection mechanism relies solely on User-Agent string matching and timing heuristics, which are trivially bypassed.

Affected Files

  • internal/tracking/worker/src/index.ts (lines 40-97)
  • internal/tracking/worker/src/bot.ts

Current Code

index.ts — No rate limiting on pixel endpoint:

async function handlePixel(request: Request, env: Env, path: string): Promise<Response> {
  const blob = path.slice(3, -4);
  const key = await importKey(env.TRACKING_KEY);
  // ... decrypts and records open with no rate check
  // Every request hits D1 database INSERT
}

bot.ts — Detection based on User-Agent strings only:

export function detectBot(userAgent: string, ip: string, timeSinceDeliveryMs: number | null): BotDetectionResult {
  if (userAgent.includes('GoogleImageProxy')) return { isBot: false, botType: 'gmail_proxy' };
  if (userAgent.includes('Outlook-iOS') || userAgent.includes('Microsoft Outlook')) return { isBot: true, botType: 'outlook_prefetch' };
  if (userAgent.includes('Barracuda') || userAgent.includes('Symantec') || userAgent.includes('Proofpoint')) return { isBot: true, botType: 'security_scanner' };
  // Trivially bypassed by setting any other User-Agent
  return { isBot: false, botType: null };
}

Risk

  1. Tracking ID enumeration: An attacker can brute-force or replay tracking pixel URLs to inflate open counts, polluting analytics
  2. D1 database abuse: Each pixel request triggers a D1 INSERT — no deduplication means repeated requests fill the database
  3. Resource exhaustion: High-volume requests against the worker endpoint consume Cloudflare Workers resources
  4. Bot detection bypass: Setting User-Agent: Mozilla/5.0 bypasses all bot detection

Remediation

  1. Add Cloudflare Rate Limiting via wrangler.toml:

    # Or use the Rate Limiting API in the worker
  2. Deduplicate opens — Only record the first open per tracking_id per IP within a time window:

    const existing = await env.DB.prepare(
      'SELECT 1 FROM opens WHERE tracking_id = ? AND ip = ? AND opened_at > datetime("now", "-1 hour")'
    ).bind(blob, ip).first();
    if (existing) return pixelResponse(); // Already recorded
  3. Add IP-based rate limiting using Cloudflare's request.cf or a KV-based counter:

    const rateKey = `rate:${ip}`;
    const count = parseInt(await env.RATE_KV.get(rateKey) || '0');
    if (count > 100) return pixelResponse(); // Silent rate limit
    await env.RATE_KV.put(rateKey, String(count + 1), { expirationTtl: 3600 });
  4. Improve bot detection — Consider checking for:

    • Missing or suspicious headers (no Accept, no Referer)
    • Known datacenter IP ranges
    • TLS fingerprinting (via Cloudflare Bot Management if available)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions