This document provides a comprehensive overview of the multi-layered abuse prevention system implemented in the Litecoin Knowledge Hub API. The system combines rate limiting, challenge-response fingerprinting, bot protection, input sanitization, cost throttling, and webhook authentication to prevent various forms of abuse.
- Architecture Overview
- Rate Limiting
- Challenge-Response Fingerprinting
- Bot Protection (Cloudflare Turnstile)
- Input Sanitization
- Cost-Based Throttling
- Webhook Authentication
- Atomic Operations (Lua Scripts)
- Integration Flow
- Configuration
- Monitoring & Metrics
The abuse prevention stack operates in multiple layers, each addressing different attack vectors:
┌─────────────────────────────────────────────────────────────┐
│ Request Flow │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. Input Sanitization │
│ - Prompt injection detection │
│ - NoSQL injection prevention │
│ - Length validation │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Challenge-Response Fingerprinting │
│ - Challenge generation & validation │
│ - Replay attack prevention │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Bot Protection (Turnstile) │
│ - CAPTCHA alternative │
│ - Graceful degradation │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Rate Limiting │
│ - Per-user sliding window │
│ - Global rate limits │
│ - Progressive bans │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. Cost-Based Throttling │
│ - Spending window limits │
│ - Daily cost caps │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. Request Processing │
└─────────────────────────────────────────────────────────────┘
The rate limiting system uses Redis-based sliding window algorithms to track and limit requests per user. It supports both per-user and global rate limits with progressive bans for repeated violations.
- Algorithm: Redis sorted sets with atomic Lua scripts
- Windows: Per-minute and per-hour limits
- Deduplication: Prevents double-counting duplicate requests (idempotency)
- Atomic Operations: All checks and updates happen in a single Redis transaction
The system extracts stable identifiers from fingerprints to ensure rate limits apply consistently:
- Fingerprint Format:
fp:challenge:hash - Stable Identifier: Extracts the
hashpart (last segment after colons) - Fallback: Uses IP address if no fingerprint provided
- IPv6 Support: Handles IPv6 addresses correctly (doesn't break on colons)
Repeated violations trigger escalating ban durations:
- 1st Violation: 1 minute ban
- 2nd Violation: 5 minutes ban
- 3rd Violation: 15 minutes ban
- 4th Violation: 60 minutes ban
Bans are tracked by IP address to prevent evasion via new challenges.
System-wide limits prevent aggregate abuse:
- Per-Minute: Default 1000 requests/minute (configurable)
- Per-Hour: Default 50,000 requests/hour (configurable)
- Admin Exemption: Admin endpoints bypass global limits
- Dynamic Configuration: Limits can be adjusted via Redis settings
Hardened IP extraction prevents spoofing attacks:
Priority Order:
CF-Connecting-IP(Cloudflare) - Always trustedX-Forwarded-For- Only trusted whenTRUST_X_FORWARDED_FOR=truerequest.client.host- Direct connection fallback
Security Features:
- IP validation (IPv4/IPv6)
- Header spoofing prevention
- Configurable trust model
# Per-endpoint rate limits
STREAM_RATE_LIMIT = RateLimitConfig(
requests_per_minute=60,
requests_per_hour=1000,
identifier="chat_stream",
enable_progressive_limits=True,
)
# Challenge endpoint (stricter)
CHALLENGE_RATE_LIMIT = RateLimitConfig(
requests_per_minute=10, # 1000 in dev
requests_per_hour=100, # 10000 in dev
identifier="challenge",
enable_progressive_limits=True,
)When rate limited, the API returns:
{
"error": "rate_limited",
"message": "Too many requests. You have been temporarily banned.",
"limits": {
"per_minute": 60,
"per_hour": 1000
},
"violation_count": 2,
"ban_expires_at": 1735689600,
"retry_after_seconds": 300
}Headers:
Retry-After: <seconds>- Time to wait before retry
Challenge-response fingerprinting prevents fingerprint replay attacks by requiring clients to include a server-generated, one-time-use challenge in their fingerprint. The system combines client-side browser fingerprinting with server-side challenge validation to create a robust user identification system.
The client generates a stable browser fingerprint using persistent browser characteristics:
Characteristics Used:
- User agent, language, platform, vendor
- Screen resolution (width/height - stable, not window size)
- Color depth, pixel depth, device pixel ratio
- Timezone offset
- Hardware concurrency, device memory
- Touch support, cookie/storage support
- Session ID (unique per browser session)
Implementation Details:
- Hash Algorithm: SHA-256 via Web Crypto API (fallback available)
- Hash Length: 32 hex characters (128 bits)
- Persistence: Stored in
localStorageto ensure consistency across page loads - Session ID: Stored in
sessionStorage(works in incognito mode) - Exclusions: Window dimensions excluded (change with resizing)
Benefits:
- Prevents multiple "unique users" from the same browser
- Stable across page reloads (via localStorage)
- Works in incognito mode (session ID persists during session)
- Low collision probability (128-bit hash)
Base Fingerprint: 32-character hex hash
hash4567890abcdef1234567890abcdef
Challenge-Response Format: fp:challenge:hash
fp:challenge123:hash4567890abcdef1234567890abcdef
Format Components:
fp:- Prefix indicating fingerprint formatchallenge- Server-generated challenge ID (64 hex chars)hash- Base browser fingerprint hash (32 hex chars)
- Client generates base fingerprint → Computes hash from browser characteristics
- Client requests challenge → Sends base fingerprint to
/api/v1/auth/challenge - Server generates challenge → Creates unique challenge ID, stores with identifier
- Client computes challenge fingerprint → Combines challenge with base hash:
fp:challenge:hash - Client sends request → Includes challenge fingerprint in
X-Fingerprintheader - Server validates → Verifies challenge exists and was issued to correct identifier
- Server consumes challenge → Challenge deleted (one-time use)
- Unique IDs: 64-character hex tokens (32 bytes)
- TTL: Configurable expiration (default: 300 seconds)
- Rate Limited: Prevents challenge exhaustion attacks
- Active Challenge Limits: Max 15 active challenges per identifier (100 in dev)
If a user requests a challenge too quickly (within rate limit window), the system:
- Checks for a recently generated challenge
- Returns the existing challenge instead of erroring
- Prevents 429 errors during rapid page loads
Similar to rate limiting, challenge generation violations trigger bans:
- 1st Violation: 1 minute ban
- 2nd Violation: 5 minutes ban
Challenges are tied to identifiers (fingerprint hash or IP) to prevent:
- Challenge sharing between users
- Replay attacks with stolen challenges
The system extracts stable identifiers from fingerprints for rate limiting and cost tracking:
Extraction Logic:
- Format:
fp:challenge:hash - Stable Identifier: Extracts the
hashpart (last segment after colons) - Purpose: Ensures rate limits apply to user, not challenge session
- IPv6 Safe: Only splits if starts with
fp:, so IPv6 addresses pass through unchanged
Example:
fingerprint = "fp:challenge123:hash456"
stable_identifier = "hash456" # Used for rate limit bucket
full_fingerprint = "fp:challenge123:hash456" # Used for deduplicationBenefits:
- Prevents bypass via new challenges (same user = same bucket)
- Allows retries (same challenge = deduplicated)
- Different challenges count separately (but same bucket)
# Settings (Redis/env)
enable_challenge_response = True # Enable/disable feature
challenge_ttl_seconds = 300 # Challenge expiration
challenge_request_rate_limit_seconds = 3 # Min time between requests
max_active_challenges_per_identifier = 15 # Max concurrent challengesFingerprints are used in a two-part identifier strategy:
-
Stable Identifier (Bucket Key): Extracted hash from fingerprint
- Used for: Redis key (
rl:chat_stream:{stable_identifier}:m) - Purpose: Rate limits apply to user, not challenge session
- Example:
hash456fromfp:challenge123:hash456
- Used for: Redis key (
-
Full Fingerprint (Deduplication ID): Complete fingerprint string
- Used for: Redis sorted set member (deduplication)
- Purpose: Prevents double-counting same request
- Example:
fp:challenge123:hash456
Benefits:
- ✅ Same user gets same rate limit bucket across challenges
- ✅ Same challenge + same request = counted once (idempotency)
- ✅ Different challenges = new request (but same bucket)
Cost throttling uses the same stable identifier extraction:
- Costs tracked by stable identifier (fingerprint hash)
- Prevents bypassing limits by getting new challenges
- Full fingerprint used for request deduplication
GET /api/v1/auth/challenge
Request Headers:
X-Fingerprint(optional): Base fingerprint hash for identifier tracking
Response:
{
"challenge": "abc123...",
"expires_in_seconds": 300
}Strengths:
- ✅ Challenge-response prevents replay attacks
- ✅ Stable identifier prevents bypass via new challenges
- ✅ One-time challenges prevent reuse
- ✅ localStorage persistence prevents multiple "users" from same browser
- ✅ IPv6 addresses handled correctly
Limitations:
⚠️ Clearing localStorage resets fingerprint (rate limits reset)⚠️ Session ID resets on browser close in incognito (expected behavior)⚠️ Client-side generation means users could modify fingerprint (mitigated by challenge-response)
Mitigations:
- Progressive bans track by IP (prevents evasion)
- Challenge validation ensures fingerprints are legitimate
- Rate limits still apply even with modified fingerprints
Cloudflare Turnstile provides invisible bot protection without traditional CAPTCHA puzzles. The system implements graceful degradation - if Turnstile fails, stricter rate limits are applied instead of blocking requests.
- No user interaction required
- Privacy-focused (no tracking)
- Fast verification (< 1 second)
If Turnstile verification fails:
- Not blocked - Request continues
- Stricter rate limits applied - 10x stricter (6/min, 60/hour)
- Logging - Failed verifications are logged for monitoring
The system handles various failure modes:
- Missing token: Falls back to strict rate limits
- API timeout: Falls back to strict rate limits
- Network errors: Falls back to strict rate limits
- Invalid token: Falls back to strict rate limits
Never returns 5xx errors - Always fails open to prevent blocking legitimate users.
# Environment variables
ENABLE_TURNSTILE=true
TURNSTILE_SECRET_KEY=your-secret-keyTurnstile verification happens after challenge validation but before rate limiting:
# In chat endpoint
if is_turnstile_enabled():
turnstile_result = await verify_turnstile_token(token, client_ip)
if not turnstile_result.get("success"):
# Apply stricter rate limits (10x)
await check_rate_limit(request, STRICT_RATE_LIMIT)
# Continue processing (don't block)Input sanitization prevents injection attacks and enforces length limits on user inputs.
Detects and neutralizes prompt injection attempts:
Detected Patterns:
ignore previous instructionsforget everythingnew instructionssystem:you are nowact as ifjailbreakroleplay- And more...
Neutralization:
- Wraps suspicious phrases:
[user input: <phrase>] - Preserves text but prevents interpretation as instructions
Prevents MongoDB injection attacks:
Escaped Characters:
- Null bytes (
\x00) - MongoDB operators (
$where,$ne,$gt, etc.) - Dollar signs followed by letters
Query Parameter Sanitization:
- Strips non-alphanumeric characters (except spaces, hyphens, underscores)
- Strict sanitization for direct query use
- Max Query Length: 400 characters (configurable)
- Truncation: Inputs exceeding limit are truncated
- Validation: Returns error if length exceeds limit
Removes dangerous control characters:
- Null bytes
- Control characters (except newlines, tabs, carriage returns)
from backend.utils.input_sanitizer import sanitize_query_input
# Comprehensive sanitization
sanitized = sanitize_query_input(user_input)
# Individual checks
is_injection, pattern = detect_prompt_injection(text)
sanitized = sanitize_prompt_injection(text)
sanitized = sanitize_nosql_injection(text)Cost-based throttling prevents abuse by tracking spending per user in sliding windows. Users exceeding spending thresholds are throttled to prevent excessive API costs.
- Window Duration: Configurable (default: 600 seconds / 10 minutes)
- Threshold: Configurable (default: $0.02 USD)
- Daily Limit: Hard cap per day (default: $0.25 USD)
Costs are tracked by stable identifier (fingerprint hash) to prevent:
- Bypassing limits by getting new challenges
- Cost accumulation across different challenge sessions
Uses full fingerprint (including challenge) for deduplication:
- Same challenge + same cost = counted once
- Different challenges = counted separately (but same stable identifier)
When threshold exceeded:
- Window Threshold: 30 seconds throttle (configurable)
- Daily Limit: 2x throttle duration (60 seconds)
- Disabled by default in development
- Can be enabled via admin dashboard or environment variable
- Admin override takes precedence over dev mode
# Settings (Redis/env)
enable_cost_throttling = True # Enable/disable
high_cost_threshold_usd = 0.02 # Window threshold
high_cost_window_seconds = 600 # Window duration
cost_throttle_duration_seconds = 30 # Throttle duration
daily_cost_limit_usd = 0.25 # Daily hard capWhen throttled:
{
"error": "cost_throttled",
"message": "High usage detected. Please complete security verification and try again in 30 seconds.",
"requires_verification": true
}Webhook authentication verifies requests from Payload CMS using HMAC-SHA256 signatures and timestamp validation to prevent replay attacks.
- Algorithm: HMAC-SHA256
- Secret: Shared secret from
WEBHOOK_SECRETenvironment variable - Constant-Time Comparison: Prevents timing attacks
- Tolerance: 5 minutes (300 seconds)
- Prevents: Replay attacks with old requests
- Validates: Request freshness
Required headers:
X-Webhook-Signature: HMAC signatureX-Webhook-Timestamp: Unix timestamp
from backend.utils.webhook_auth import verify_webhook_request
# In webhook endpoint
is_valid, error = await verify_webhook_request(request, body_bytes)
if not is_valid:
raise HTTPException(status_code=401, detail=error)Redis Lua scripts ensure atomic operations for rate limiting and cost throttling, eliminating race conditions where concurrent requests could bypass limits.
Atomic sliding window rate limit check:
Operations:
- Clean expired entries
- Count current requests
- Check if limit exceeded
- Add new request (if allowed)
- Return result
Returns:
[allowed (1/0), count, oldest_timestamp]
Deduplication:
- If member exists: Update timestamp, allow (idempotent)
- If under limit: Add new member, allow
- If over limit: Reject, return oldest timestamp for retry calculation
Atomic cost throttling check:
Operations:
- Check if already throttled
- Clean expired entries from window
- Calculate total cost in window
- Calculate total daily cost
- Check daily limit (hard cap)
- Check window threshold
- Record cost (if allowed)
Returns:
[status_code, ttl_or_duration]0: Allowed1: Already throttled (returns remaining TTL)2: Daily limit exceeded (returns throttle duration)3: Window threshold exceeded (returns throttle duration)
Atomic cost recording (after LLM call completes):
Operations:
- Record in window ZSET
- Record in daily ZSET
- Set TTLs
Use Case:
- Updates cost tracking with actual cost (replaces estimated cost)
- Atomicity: All operations in single Redis transaction
- Race Condition Prevention: Concurrent requests can't bypass limits
- Performance: Single round-trip to Redis
- Consistency: Guaranteed accurate counts
1. Request arrives
↓
2. Input Sanitization
- Detect prompt injection
- Sanitize NoSQL injection
- Validate length
↓
3. Challenge Validation
- Extract challenge from fingerprint
- Validate challenge exists
- Verify challenge issued to correct identifier
- Consume challenge (one-time use)
↓
4. Turnstile Verification (if enabled)
- Verify token with Cloudflare
- On failure: Apply strict rate limits (don't block)
↓
5. Rate Limiting
- Extract stable identifier
- Check progressive bans
- Check global rate limits
- Check per-user rate limits (atomic)
↓
6. Cost-Based Throttling
- Estimate cost
- Check spending window
- Check daily limit
- Record estimated cost (atomic)
↓
7. Process Request
- RAG pipeline
- LLM call
- Record actual cost
↓
8. Return Response
1. Request arrives
↓
2. Rate Limiting (challenge-specific limits)
- Extract identifier (stable hash or IP)
- Check progressive bans
↓
3. Challenge Generation
- Check active challenge count
- Check rate limit (time since last request)
- Generate unique challenge ID
- Store in Redis with TTL
↓
4. Return Challenge
# Rate Limiting
RATE_LIMIT_PER_MINUTE=60
RATE_LIMIT_PER_HOUR=1000
ENABLE_GLOBAL_RATE_LIMIT=true
GLOBAL_RATE_LIMIT_PER_MINUTE=1000
GLOBAL_RATE_LIMIT_PER_HOUR=50000
TRUST_X_FORWARDED_FOR=false # Only true behind trusted proxy
# Challenge-Response
ENABLE_CHALLENGE_RESPONSE=true
CHALLENGE_TTL_SECONDS=300
CHALLENGE_REQUEST_RATE_LIMIT_SECONDS=3
MAX_ACTIVE_CHALLENGES_PER_IDENTIFIER=15
# Turnstile
ENABLE_TURNSTILE=true
TURNSTILE_SECRET_KEY=your-secret-key
# Cost Throttling
ENABLE_COST_THROTTLING=true
HIGH_COST_THRESHOLD_USD=0.02
HIGH_COST_WINDOW_SECONDS=600
COST_THROTTLE_DURATION_SECONDS=30
DAILY_COST_LIMIT_USD=0.25
# Webhook Auth
WEBHOOK_SECRET=your-webhook-secretAll settings can be dynamically updated via Redis (admin dashboard):
enable_global_rate_limitglobal_rate_limit_per_minuteglobal_rate_limit_per_hourenable_challenge_responsechallenge_ttl_secondsenable_cost_throttlinghigh_cost_threshold_usddaily_cost_limit_usd- And more...
Settings are read with environment variable fallbacks - Redis takes precedence.
rate_limit_rejections_total- Total rejections by endpointrate_limit_bans_total- Total bans appliedrate_limit_violations_total- Total violationsrate_limit_retry_after_seconds- Retry-after times (histogram)rate_limit_checks_total- Total checks (allowed/rejected)
challenge_generation_total- Challenge generations (success/rate_limited/banned)challenge_validations_total- Challenge validations (success/failure)challenge_validation_failures_total- Validation failures by reasonchallenge_reuse_attempts_total- Replay attack attempts
cost_throttle_triggers_total- Throttle triggers by reasoncost_throttle_active_users- Currently throttled users (gauge)cost_recorded_usd_total- Total cost recorded (estimated/actual)
lua_script_executions_total- Script executions (success/error)lua_script_duration_seconds- Script execution duration (histogram)
All abuse prevention events are logged:
- Rate limit violations: Warning level
- Challenge validation failures: Warning level
- Turnstile failures: Warning level
- Cost throttling triggers: Warning level
- Progressive bans: Warning level
Consider setting up alerts for:
- High rate limit rejection rates
- Frequent challenge validation failures
- Cost throttling triggers
- Lua script errors
- Always use Cloudflare when possible -
CF-Connecting-IPcannot be spoofed - Never trust
X-Forwarded-Forwithout a properly configured reverse proxy - Enable challenge-response in production to prevent fingerprint replay
- Monitor cost throttling to detect abuse patterns
- Review rate limit settings regularly based on traffic patterns
- Test progressive bans to ensure they're working correctly
- Monitor Lua script errors - they indicate Redis issues
- Keep webhook secrets secure - rotate regularly
- Enable Turnstile for additional bot protection
- Review input sanitization logs for injection attempts
Symptoms:
- Users bypassing rate limits
- Limits not enforced
Possible Causes:
TRUST_X_FORWARDED_FOR=truebut reverse proxy not configured- Redis connection issues
- Lua script errors
Solutions:
- Verify reverse proxy strips user-supplied headers
- Check Redis connectivity
- Review Lua script error logs
Symptoms:
- Challenge validation always fails
- "Invalid challenge" errors
Possible Causes:
- Challenge expired (TTL too short)
- Redis connection issues
- Challenge consumed twice (race condition)
Solutions:
- Increase
CHALLENGE_TTL_SECONDS - Check Redis connectivity
- Review challenge validation logs
Symptoms:
- Legitimate users getting throttled
- Thresholds too low
Solutions:
- Increase
HIGH_COST_THRESHOLD_USD - Increase
DAILY_COST_LIMIT_USD - Review cost estimation accuracy
- Fingerprinting Review - Detailed fingerprinting implementation review
- Rate Limiting Security - IP spoofing prevention
- Red Team Assessment - Security audit results
- Environment Variables - Configuration reference