Skip to content

Production-ready Caddy HTTP middleware module for ALTCHA captcha verification. Protects web applications from automated abuse using cryptographic proof-of-work challenges.

License

Notifications You must be signed in to change notification settings

stardothosting/caddy-altcha

Repository files navigation

Caddy ALTCHA Module

Production-ready Caddy HTTP middleware for ALTCHA captcha verification. Protects web applications from automated abuse using cryptographic proof-of-work challenges.

Architecture

ALTCHA is entirely self-contained within your Caddy server. No separate ALTCHA service or external API calls are required. The module:

  1. Generates cryptographic challenges via /api/altcha/challenge endpoint
  2. Verifies solutions submitted by clients
  3. Manages sessions using Memory, Redis, or File storage backends

The only external dependency is the ALTCHA JavaScript widget (can be self-hosted). All cryptographic operations happen within your Caddy module.

Features

Core Functionality:

  • Cryptographic protection using HMAC-signed proof-of-work challenges
  • Sub-10ms verification latency
  • Multiple storage backends (Redis, Memory, File)
  • POST data preservation across verification redirects
  • Automatic return URI preservation (users return to originally requested page)

Security & Protection:

  • Rate limiting per IP (DoS protection)
  • CORS origin validation (resource protection)
  • Constant-time HMAC comparison (timing attack prevention)
  • Secure session management with 256-bit entropy
  • Safe cookie defaults (Secure, HttpOnly, SameSite)
  • Input validation and payload size limits

Integration & Deployment:

  • Coraza WAF integration support
  • Production-ready with comprehensive error handling
  • Distributed deployments via Redis backend
  • Environment variable configuration
  • JSON and Caddyfile configuration support
  • Docker-ready with examples

Installation

Build Caddy with the ALTCHA module using a specific Caddy version (recommended for stability):

xcaddy build v2.8.4 --with github.com/stardothosting/caddy-altcha

Or build with the latest Caddy:

xcaddy build --with github.com/stardothosting/caddy-altcha

Or using Docker:

cd examples
docker-compose up

Note: Using a specific Caddy version (e.g., v2.8.4) prevents compatibility issues with newer Go versions and ensures build reproducibility.

Basic Configuration

Create a Caddyfile:

{
    order altcha_verify before reverse_proxy
}

example.com {
    # Challenge generation endpoint
    route /api/altcha/challenge {
        altcha_challenge {
            hmac_key {env.ALTCHA_HMAC_KEY}
            algorithm SHA-256
            max_number 100000
            expires 5m
        }
    }
    
    # Challenge UI page
    route /captcha {
        root * /var/www/altcha
        file_server
    }
    
    # Protect specific routes
    @protected {
        path /login /register /api/*
    }

    # IMPORTANT: defining a matcher alone does nothing.
    # You must attach it to a handler (altcha_verify or handle block),
    # otherwise requests will fall through to reverse_proxy.
    
    altcha_verify @protected {
        hmac_key {env.ALTCHA_HMAC_KEY}
        session_backend memory://
        challenge_redirect /captcha
        preserve_post_data true
    }
    
    reverse_proxy localhost:8080
}

Set your HMAC key (minimum 32 characters):

export ALTCHA_HMAC_KEY="your-secret-key-min-32-chars-long"

Copy the example HTML to your web root:

cp examples/www/index.html /var/www/altcha/index.html

Challenge Page Setup

Create an HTML page at /var/www/altcha/index.html with the ALTCHA widget. This page will be shown to users when they need to complete a challenge.

Minimal Example (Proof-of-Work Only)

<!DOCTYPE html>
<html>
<head>
    <title>Verification Required</title>
    <script type="module" src="https://cdn.jsdelivr.net/npm/altcha@1.0.5/dist/altcha.min.js"></script>
</head>
<body>
    <h1>Verification Required</h1>
    <p>Please complete the challenge below.</p>
    
    <altcha-widget
        name="altcha"
        challengeurl="/api/altcha/challenge"
        hidefooter="false">
    </altcha-widget>
    
    <script>
        const widget = document.querySelector('altcha-widget');
        widget.addEventListener('statechange', (ev) => {
            if (ev.detail.state === 'verified') {
                const payload = ev.detail.payload;
                const urlParams = new URLSearchParams(window.location.search);
                const session = urlParams.get('session');
                const returnTo = urlParams.get('return') || '/';
                
                // Build redirect URL with solution
                let redirectURL = `${returnTo}?altcha=${encodeURIComponent(payload)}`;
                if (session) {
                    redirectURL += `&session=${encodeURIComponent(session)}`;
                }
                
                // Redirect back to original protected page
                // The same altcha_verify handler will now verify the solution
                window.location.href = redirectURL;
            }
        });
    </script>
</body>
</html>

Production-Ready Example (with Styling)

A complete, styled example is available in examples/www/index.html. Key features:

  • Modern responsive design with Poppins font
  • Cache control headers to prevent stale page loads
  • Status messages for user feedback (solving, verified, error)
  • Logo support with embedded SVG
  • Mobile-friendly layout

Copy the example to your web root:

cp examples/www/index.html /var/www/altcha/index.html

Or view the full source: examples/www/index.html

Critical JavaScript Requirements

You MUST include this redirect logic in your challenge page to properly handle URLs with existing query parameters (WordPress login, Drupal, Magento, Laravel, etc.):

/**
 * Build a safe redirect URL that properly handles existing query parameters.
 * Works with any CMS: WordPress, Drupal, Magento, Laravel, etc.
 */
function buildRedirectURL(returnTo, payload, session) {
    try {
        // Parse the return URL using the URL API
        // This handles all edge cases: existing query params, fragments, encoding
        const url = new URL(returnTo, window.location.origin);
        
        // Security: Only allow same-origin redirects to prevent open redirect attacks
        if (url.origin !== window.location.origin) {
            console.error('Security: Cross-origin redirect blocked:', returnTo);
            return '/?altcha=' + encodeURIComponent(payload);
        }
        
        // Use searchParams.set() to properly append/merge query parameters
        // This automatically uses & if params exist, or ? if not
        url.searchParams.set('altcha', payload);
        if (session) {
            url.searchParams.set('session', session);
        }
        
        // Return relative URL (pathname + search + hash) for same-origin redirect
        return url.pathname + url.search + url.hash;
        
    } catch (e) {
        // Fallback for malformed URLs - redirect to root with payload
        console.error('Invalid return URL, using fallback:', returnTo, e);
        return '/?altcha=' + encodeURIComponent(payload);
    }
}

// Usage in your statechange handler:
const urlParams = new URLSearchParams(window.location.search);
const returnTo = urlParams.get('return') || '/';
const session = urlParams.get('session');

// After widget verification succeeds
const redirectURL = buildRedirectURL(returnTo, payload, session);
window.location.href = redirectURL;

Why the URL API is required:

When protecting pages with existing query parameters (like WordPress login):

  • Original URL: /wp-login.php?redirect_to=...&reauth=1
  • Bad (string concat): /wp-login.php?redirect_to=...&reauth=1?altcha=... (broken - double ?)
  • Good (URL API): /wp-login.php?redirect_to=...&reauth=1&altcha=... (correct)

The URL API automatically uses & when query params exist, or ? when they don't.

Flow explanation:

  1. User requests /admin/settings (protected by altcha_verify)
  2. Module redirects to /captcha?session=xyz&return=/admin/settings
  3. User solves challenge
  4. JavaScript reads return parameter and redirects to /admin/settings?altcha=payload
  5. Same handler on /admin/settings verifies solution and lets user through

Without proper URL handling, users will experience redirect loops when the protected page has query parameters.

Optional: Code Challenges (Visual CAPTCHA)

Add visual code input on top of proof-of-work:

<script type="module" src="https://cdn.jsdelivr.net/npm/altcha@1.0.5/dist/altcha.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/altcha@1.0.5/obfuscation"></script>

<altcha-widget
    name="altcha"
    challengeurl="/api/altcha/challenge"
    plugins="obfuscation"
    hidefooter="false">
</altcha-widget>

Then enable in your handler config: code_challenge: true

Important Notes

  • Pin the widget version (e.g., @1.0.5) to prevent breaking changes from automatic updates
  • Do NOT add verifyurl attribute - that's for ALTCHA Sentinel cloud service, not self-hosted
  • The widget solves challenges client-side - it generates a proof-of-work solution in the browser
  • Cache control headers are recommended to prevent users from seeing stale challenge pages

Configuration Reference

altcha_challenge

Generates cryptographic challenges for clients to solve.

altcha_challenge {
    hmac_key <string>           # Required: HMAC secret key (min 32 chars recommended)
    algorithm <string>          # SHA-256, SHA-384, or SHA-512 (default: SHA-256)
    max_number <int>            # Maximum random number (default: 100000)
                                # Recommended: 1000000 (~200ms solve time)
    expires <duration>          # Challenge validity (default: 5m)
    salt_length <int>           # Salt length in bytes (default: 12)
    code_challenge <bool>       # Enable visual code challenges (default: false)
    code_length <int>           # Visual code length (default: 6, requires code_challenge: true)
    
    # Security (Optional)
    rate_limit_requests <int>   # Max requests per window (0 = disabled, default: 0)
    rate_limit_window <duration># Rate limit window (default: 1m)
}

Difficulty Tuning (max_number):

  • 100000 - Very fast (~20ms solve time), light protection
  • 1000000 - Recommended (~200ms solve time), good balance
  • 10000000 - High security (~2s solve time), may frustrate users

Code Challenges (Optional):

  • Set code_challenge: true to add visual code input on top of proof-of-work
  • Similar to traditional CAPTCHAs but with computational challenge first
  • Requires ALTCHA obfuscation plugin in your HTML (see Widget Configuration)

Rate Limiting (Optional):

  • rate_limit_requests: 10 - Max 10 challenges per IP per window
  • rate_limit_window: 1m - Reset window duration
  • Prevents DoS attacks via excessive challenge generation
  • Returns HTTP 429 (Too Many Requests) when limit exceeded

altcha_verify

Verifies ALTCHA solutions and manages verification state.

altcha_verify [<matcher>] {
    hmac_key <string>                  # Required: Same key as altcha_challenge
    session_backend <uri>              # Session storage (default: memory://)
    session_ttl <duration>             # Session validity (default: 5m)
    
    # Verification cookie configuration
    verified_cookie_name <string>      # Cookie name (default: altcha_verified)
    verified_cookie_ttl <int>          # Cookie TTL in seconds (default: 3600)
    verified_cookie_secure <bool>      # Secure flag (default: true)
    verified_cookie_http_only <bool>   # HttpOnly flag (default: true)
    verified_cookie_same_site <string> # SameSite: Strict, Lax, None (default: Strict)
    verified_cookie_path <string>      # Cookie path (default: /)
    verified_cookie_domain <string>    # Cookie domain (optional)
    
    # Behavior
    challenge_redirect <path>          # Challenge page path (default: /captcha)
    preserve_post_data <bool>          # Save POST data across redirects (default: false)
    verify_field_name <string>         # Form field with solution (default: altcha)
    
    # Integrations
    coraza_env_var <string>            # Environment variable set by Coraza
}

altcha_verify_solution (Optional)

Provides a dedicated endpoint for widget-based server-side verification. This is optional and only needed if you're using the ALTCHA widget with verifyurl attribute (typically for ALTCHA Sentinel cloud service).

altcha_verify_solution {
    hmac_key <string>               # Required: Same key as altcha_challenge
    allowed_origins <string...>     # Allowed CORS origins (optional, restricts widget usage)
}

CORS Security:

  • By default (no allowed_origins), allows requests from any origin (backward compatible)
  • Configure allowed_origins to restrict which domains can use your challenges
  • Example: allowed_origins https://yourdomain.com https://test.yourdomain.com
  • Prevents unauthorized sites from consuming your server resources

Security Best Practices

Production Configuration

{
  "handler": "altcha_challenge",
  "hmac_key": "{env.ALTCHA_HMAC_KEY}",
  "algorithm": "SHA-256",
  "max_number": 1000000,
  "expires": "5m",
  "rate_limit_requests": 10,
  "rate_limit_window": "1m"
}
{
  "handler": "altcha_verify",
  "hmac_key": "{env.ALTCHA_HMAC_KEY}",
  "session_backend": "redis://localhost:6379",
  "session_ttl": "5m",
  "verified_cookie_secure": true,
  "verified_cookie_http_only": true,
  "verified_cookie_same_site": "Strict"
}

HMAC Key Generation

# Generate cryptographically secure 256-bit key
openssl rand -base64 32

# Store in environment variable
export ALTCHA_HMAC_KEY="your-generated-key-here"

Key Requirements:

  • Minimum 32 bytes (256 bits) for strong security
  • Use crypto/rand or similar CSPRNG for generation
  • Never hardcode keys in configuration files
  • Rotate keys periodically (implement key rotation strategy)
  • Store securely (environment variables, secrets manager, or vault)

Rate Limiting

Protect against resource exhaustion attacks:

altcha_challenge {
    hmac_key {env.ALTCHA_HMAC_KEY}
    max_number 1000000
    rate_limit_requests 10      # Max 10 challenges per IP
    rate_limit_window 1m        # Per 60-second window
}

Recommendations:

  • Development: Disable rate limiting (rate_limit_requests: 0)
  • Production: Start with 10-20 requests/minute
  • High-traffic: Increase to 50-100 requests/minute if needed
  • Monitor 429 Too Many Requests responses to tune appropriately

CORS Restriction

Prevent unauthorized widget usage:

{
  "handler": "altcha_verify_solution",
  "hmac_key": "{env.ALTCHA_HMAC_KEY}",
  "allowed_origins": [
    "https://yourdomain.com",
    "https://www.yourdomain.com"
  ]
}

Warning: Without allowed_origins, any website can embed your challenge widget and consume your server resources.

Session Security

altcha_verify {
    session_backend redis://localhost:6379
    session_ttl 5m              # Short TTL reduces exposure window
    verified_cookie_secure true # HTTPS only
    verified_cookie_http_only true # Prevent XSS access
    verified_cookie_same_site Strict # Prevent CSRF
}

Production Backend Recommendations:

  • Memory: Development only (sessions lost on restart)
  • File: Simple deployments, single server
  • Redis: Production, distributed deployments, high availability

Session Backends

Memory (Development)

In-memory storage with LRU eviction:

session_backend memory://

Redis (Production)

Distributed storage with connection pooling:

session_backend redis://localhost:6379/0
session_backend redis://:password@localhost:6379/0

File (Simple Persistence)

File-based storage:

session_backend file:///var/lib/caddy/altcha

Usage Examples

Protect Authentication Routes

@auth {
    path /login /register /reset-password
}

altcha_verify @auth {
    hmac_key {env.ALTCHA_HMAC_KEY}
    session_backend redis://localhost:6379/0
    challenge_redirect /captcha
}

Protect API with POST Preservation

@api_post {
    path /api/*
    method POST
}

altcha_verify @api_post {
    hmac_key {env.ALTCHA_HMAC_KEY}
    preserve_post_data true
    challenge_redirect /captcha
}

Skip Internal Traffic

@external {
    not remote_ip 10.0.0.0/8 192.168.0.0/16
}

altcha_verify @external {
    hmac_key {env.ALTCHA_HMAC_KEY}
    challenge_redirect /captcha
}

JSON Configuration for Production

For production deployments using JSON configuration (common in WAF-as-a-service setups):

Complete Test Endpoint Example

{
  "apps": {
    "http": {
      "servers": {
        "main": {
          "listen": [":443"],
          "routes": [
            {
              "match": [{"host": ["test.example.com"]}],
              "handle": [{
                "handler": "subroute",
                "routes": [
                  {
                    "match": [{
                      "path_regexp": {
                        "pattern": "^/api/altcha/challenge/?$"
                      }
                    }],
                    "handle": [{
                      "handler": "altcha_challenge",
                      "hmac_key": "{env.ALTCHA_HMAC_KEY}",
                      "algorithm": "SHA-256",
                      "max_number": 1000000,
                      "expires": "5m"
                    }],
                    "terminal": true
                  },
                  {
                    "match": [{
                      "path_regexp": {
                        "pattern": "^/captcha/?$"
                      }
                    }],
                    "handle": [
                      {
                        "handler": "rewrite",
                        "uri": "/index.html"
                      },
                      {
                        "handler": "file_server",
                        "root": "/var/www/altcha"
                      }
                    ],
                    "terminal": true
                  },
                  {
                    "match": [{
                      "path_regexp": {
                        "pattern": "^/protected/?$"
                      }
                    }],
                    "handle": [
                      {
                        "handler": "altcha_verify",
                        "hmac_key": "{env.ALTCHA_HMAC_KEY}",
                        "session_backend": "memory://",
                        "challenge_redirect": "/captcha",
                        "preserve_post_data": true
                      },
                      {
                        "handler": "static_response",
                        "body": "{\"success\": true, \"message\": \"Verification passed\"}"
                      }
                    ]
                  }
                ]
              }]
            }
          ]
        }
      }
    }
  }
}

Important JSON Configuration Notes:

  • Use path_regexp with pattern ^/path/?$ to handle both /path and /path/ (trailing slash)
  • Set terminal: true for routes that should not fall through
  • Use rewrite before file_server to serve index.html at /captcha
  • Recommended max_number: 1000000 for ~200ms solve time

WAF-Protected Site Integration

For sites behind Coraza WAF, add ALTCHA routes to existing configurations:

{
  "@id": "example.com",
  "match": [{"host": ["example.com"]}],
  "handle": [{
    "handler": "subroute",
    "routes": [
      {
        "@comment": "Coraza WAF - runs first",
        "handle": [{
          "handler": "waf",
          "directives": "SecComponentSignature \"WAF | example.com\"\nSecRuleEngine On\n..."
        }]
      },
      {
        "@comment": "ALTCHA Challenge Endpoint",
        "match": [{"path": ["/api/altcha/challenge"]}],
        "handle": [{
          "handler": "altcha_challenge",
          "hmac_key": "{env.ALTCHA_HMAC_KEY}",
          "algorithm": "SHA-256",
          "max_number": 100000,
          "expires": "5m"
        }]
      },
      {
        "@comment": "ALTCHA Challenge UI",
        "match": [{"path": ["/captcha"]}],
        "handle": [{
          "handler": "file_server",
          "root": "/var/www/altcha"
        }]
      },
      {
        "@comment": "Protected routes requiring ALTCHA",
        "match": [{"path": ["/login", "/register", "/api/submit"]}],
        "handle": [
          {
            "handler": "altcha_verify",
            "hmac_key": "{env.ALTCHA_HMAC_KEY}",
            "session_backend": "redis://localhost:6379/1",
            "session_ttl": "5m",
            "verified_cookie_name": "altcha_verified",
            "verified_cookie_ttl": 3600,
            "challenge_redirect": "/captcha",
            "preserve_post_data": true,
            "coraza_env_var": "altcha_required"
          },
          {
            "handler": "reverse_proxy",
            "upstreams": [{"dial": "backend:443"}]
          }
        ]
      },
      {
        "@comment": "Unprotected routes bypass ALTCHA",
        "handle": [{
          "handler": "reverse_proxy",
          "upstreams": [{"dial": "backend:443"}]
        }]
      }
    ]
  }],
  "terminal": true
}

Deployment Steps

  1. Generate HMAC key:
openssl rand -base64 32
  1. Create challenge UI directory:
mkdir -p /var/www/altcha
  1. Create /var/www/altcha/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Verification Required</title>
    <style>
        body {
            font-family: system-ui, -apple-system, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            background: #f5f5f5;
        }
        .container {
            background: white;
            padding: 2rem;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            max-width: 500px;
            text-align: center;
        }
        h1 { margin-top: 0; color: #333; }
        p { color: #666; line-height: 1.6; }
        #altcha-widget { margin: 2rem 0; }
        .error { color: #d32f2f; margin-top: 1rem; }
        .loading { color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Verification Required</h1>
        <p>Please complete the challenge below to continue.</p>
        
        <form id="altcha-form" method="POST">
            <div id="altcha-widget"></div>
            <div id="status" class="loading">Loading challenge...</div>
        </form>
    </div>

    <script type="module">
        import 'altcha' from 'https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js';

        const widget = document.createElement('altcha-widget');
        widget.setAttribute('challengeurl', '/api/altcha/challenge');
        widget.setAttribute('auto', 'onload');
        
        document.getElementById('altcha-widget').appendChild(widget);
        document.getElementById('status').textContent = 'Solving challenge...';

        widget.addEventListener('statechange', (ev) => {
            const state = ev.detail.state;
            const status = document.getElementById('status');
            
            if (state === 'verified') {
                status.textContent = 'Verified! Redirecting...';
                status.style.color = '#2e7d32';
                
                const payload = ev.detail.payload;
                const urlParams = new URLSearchParams(window.location.search);
                const sessionId = urlParams.get('session');
                const returnTo = urlParams.get('return') || '/';  // Where to redirect after verification
                
                // Build redirect URL back to original protected page
                let redirectUrl = `${returnTo}?altcha=${encodeURIComponent(payload)}`;
                if (sessionId) {
                    redirectUrl += `&session=${encodeURIComponent(sessionId)}`;
                }
                
                setTimeout(() => window.location.href = redirectUrl, 500);
            } else if (state === 'error') {
                status.textContent = 'Verification failed. Please refresh and try again.';
                status.className = 'error';
            }
        });
    </script>
</body>
</html>
  1. Set environment variable:
export ALTCHA_HMAC_KEY="your-generated-key-here"
  1. Build Caddy with ALTCHA module:
xcaddy build --with github.com/stardothosting/caddy-altcha \
  --with github.com/corazawaf/coraza-caddy/v2
  1. Load configuration:
caddy reload --config /path/to/config.json
  1. Test endpoints:
# Health check
curl https://test.example.com/health

# Challenge generation
curl https://test.example.com/api/altcha/challenge

# Protected route (should redirect)
curl -I https://test.example.com/protected

Notes on File Server Behavior

The file_server handler automatically serves index.html when a directory is requested. When configured with:

{
  "handler": "file_server",
  "root": "/var/www/altcha"
}

Requesting /captcha will serve /var/www/altcha/index.html.

HMAC Key Management

The HMAC key must be identical in both altcha_challenge and altcha_verify handlers. Options:

  1. Environment variable (recommended):
"hmac_key": "{env.ALTCHA_HMAC_KEY}"
  1. Hardcoded (less secure, avoid in production):
"hmac_key": "your-actual-key-here"
  1. Per-site keys (for multi-tenant setups):
"hmac_key": "{env.ALTCHA_HMAC_KEY_SITE1}"

Coraza WAF Integration

Use Coraza to analyze requests and only challenge suspicious ones:

{
    order coraza_waf before altcha_verify
    order altcha_verify before reverse_proxy
}

example.com {
    # Coraza analyzes requests
    coraza_waf {
        directives `
            Include /etc/coraza/crs-setup.conf
            Include /etc/coraza/rules/*.conf
            
            # Flag suspicious requests
            SecRule TX:ANOMALY_SCORE "@ge 5" \
                "id:1001,phase:2,pass,setenv:altcha_required=1"
        `
    }
    
    route /api/altcha/challenge {
        altcha_challenge {
            hmac_key {env.ALTCHA_HMAC_KEY}
        }
    }
    
    route /captcha {
        root * /var/www/altcha
        file_server
    }
    
    # Only challenge flagged requests
    altcha_verify {
        hmac_key {env.ALTCHA_HMAC_KEY}
        coraza_env_var altcha_required
        session_backend redis://localhost:6379/0
        challenge_redirect /captcha
    }
    
    reverse_proxy backend:8080
}

Performance

  • Verification latency: Sub-10ms per request
  • Session capacity: 10,000+ concurrent sessions (memory backend)
  • Redis pooling: MaxIdle 10, MaxActive 100
  • Zero blocking operations in hot path

Security

This module has undergone comprehensive security auditing and implements industry best practices for cryptographic operations, session management, and DoS protection.

Security Features

Cryptographic Security:

  • HMAC-SHA256/384/512 signatures for challenge integrity
  • Constant-time HMAC comparison to prevent timing attacks
  • Cryptographically random session ID generation (256-bit entropy)
  • Hex-encoded session IDs (64 characters) for consistent length

Request Protection:

  • Rate limiting per IP address (configurable sliding window)
  • Payload length validation (4KB max, early rejection)
  • Input sanitization for all user-provided data
  • Safe redirect validation to prevent open redirect attacks

Session Security:

  • Atomic write-rename pattern for file backend (prevents race conditions)
  • Server-side session storage (no sensitive data in URLs)
  • Configurable session TTL (default 5 minutes)
  • One-time session usage for return URI restoration

Cookie Security:

  • Secure flag enforced (HTTPS only)
  • HttpOnly flag (prevents XSS access)
  • SameSite: Strict (prevents CSRF)
  • Configurable domain and path restrictions

CORS Protection:

  • Configurable origin whitelist for challenge endpoint
  • Prevents unauthorized widget embedding
  • Protects against resource consumption attacks

Error Handling:

  • Generic error messages to clients (no information disclosure)
  • Detailed logging internally for debugging
  • No payload or sensitive data exposure in responses

Security Best Practices

  1. HMAC Keys: Use cryptographically random keys (minimum 32 bytes)
  2. Rate Limiting: Enable in production (rate_limit_requests: 10)
  3. CORS Origins: Restrict to your domains (allowed_origins: ["https://yourdomain.com"])
  4. Session Backend: Use Redis in production for distributed security
  5. Cookie Flags: Keep secure defaults enabled
  6. HTTPS Only: Always use TLS in production
  7. Key Rotation: Implement periodic HMAC key rotation
  8. Monitoring: Track 429 (rate limit) responses to tune limits

Key Generation

# Generate a cryptographically secure 256-bit HMAC key
openssl rand -base64 32

Environment Variables

export ALTCHA_HMAC_KEY="$(openssl rand -base64 32)"

Security Audit

All HIGH and MEDIUM severity vulnerabilities identified in November 2025 security audit have been addressed:

  • ✅ Session ID predictability eliminated
  • ✅ CORS wildcard replaced with origin validation
  • ✅ File backend race conditions fixed
  • ✅ Error message information disclosure prevented
  • ✅ Rate limiting implemented
  • ✅ Timing attack vectors mitigated
  • ✅ Payload bomb attacks prevented

Troubleshooting

Challenge Not Loading

Check that the challenge endpoint is accessible:

curl https://example.com/api/altcha/challenge

Challenge Never Appears / Request Passes Through

Cause: The matcher was defined but never attached to altcha_verify, or the handler runs after reverse_proxy.

Fix: Attach the matcher and ensure it runs before reverse_proxy:

# ❌ Wrong: matcher defined but never used
@protected path /login /register /api/*
reverse_proxy localhost:8080

# ✅ Correct: matcher attached to altcha_verify
@protected path /login /register /api/*
altcha_verify @protected {
    hmac_key {env.ALTCHA_HMAC_KEY}
    session_backend memory://
    challenge_redirect /captcha
}
reverse_proxy localhost:8080

Verification Failing

Ensure HMAC keys match between challenge and verify handlers:

# Both must use the same key
altcha_challenge {
    hmac_key {env.ALTCHA_HMAC_KEY}
}

altcha_verify {
    hmac_key {env.ALTCHA_HMAC_KEY}
}

POST Data Lost

Enable POST data preservation:

altcha_verify {
    preserve_post_data true
}

User Redirected to Wrong Page After Challenge

Symptom: After solving the challenge, user lands on root page (/) or previous page instead of intended destination.

Cause: Challenge page JavaScript not reading the return parameter.

Fix: Update your challenge page JavaScript to read the return parameter:

const urlParams = new URLSearchParams(window.location.search);
const returnTo = urlParams.get('return') || '/';  // Read destination
const payload = ev.detail.payload;
const session = urlParams.get('session');

// Redirect to original protected page
let redirectURL = `${returnTo}?altcha=${encodeURIComponent(payload)}`;
if (session) {
    redirectURL += `&session=${encodeURIComponent(session)}`;
}
window.location.href = redirectURL;

How it works:

  1. User clicks /admin/settings (protected)
  2. Handler redirects to /captcha?session=xyz&return=/admin/settings
  3. User solves challenge
  4. JavaScript reads return param and redirects to /admin/settings?altcha=payload
  5. Same handler on /admin/settings verifies solution and lets user through

Key point: The altcha_verify handler on the protected page handles both the initial redirect AND the verification on return.

Automatic Return URI Preservation

The module automatically preserves the original request URI when redirecting users to the challenge page.

How it works:

  1. User requests /wp-login.php without verification
  2. Module stores /wp-login.php in session backend (secure, server-side)
  3. Module redirects to /captcha?session=<session-id>&return=/wp-login.php
  4. User solves challenge
  5. Widget reads return param and redirects to /wp-login.php?altcha=<payload>&session=<session-id>
  6. Module retrieves return URI from session, verifies solution
  7. Module continues request or redirects to clean URL

Security benefits:

  • Return URI stored server-side in session backend (not in URL)
  • No URL parameter tampering possible
  • Prevents open redirect attacks
  • Session is one-time use (deleted after retrieval)

No configuration needed - this behavior is automatic. Sessions are required for this feature (use memory://, file://, or redis:// backend).

Redis Connection Failed

Check Redis is running and URI is correct:

redis-cli -u redis://localhost:6379/0 ping

Blank Page at /captcha/

If /captcha works but /captcha/ shows a blank page, your route isn't handling trailing slashes.

Fix: Use path_regexp in JSON config:

{
  "match": [{
    "path_regexp": {
      "pattern": "^/captcha/?$"
    }
  }]
}

Or in Caddyfile, use wildcards:

route /captcha* {
    rewrite * /index.html
    root * /var/www/altcha
    file_server
}

Widget Shows "Verification Failed"

This usually indicates a protocol mismatch. Check:

# 1. Verify challenge endpoint returns correct JSON
curl https://example.com/api/altcha/challenge | jq

# Expected fields:
# - algorithm (string)
# - challenge (string - hex hash)
# - maxNumber (int - MUST be camelCase, not maxnumber!)
# - salt (string - hex)
# - signature (string - HMAC of challenge only)

# 2. Check browser console for errors
# 3. Verify widget is NOT using verifyurl attribute (self-hosted mode)
# 4. Hard refresh browser: Ctrl+Shift+R

Development

Build from Source

git clone https://github.com/stardothosting/caddy-altcha.git
cd caddy-altcha
go mod download
xcaddy build v2.8.4 --with github.com/stardothosting/caddy-altcha=.

Run Tests

# Run all tests
make test

# Or run individually:
go test ./...
go test -race ./...
go test -bench=. -benchmem

Run Example

cd examples
export ALTCHA_HMAC_KEY="test-key-min-32-characters-long"
docker-compose up

Visit http://localhost/captcha to test the challenge UI.

License

MIT License - see LICENSE file for details

Credits

  • ALTCHA - Proof-of-work captcha library
  • Caddy - Web server and reverse proxy
  • caddy-defender - Module architecture inspiration

About

Production-ready Caddy HTTP middleware module for ALTCHA captcha verification. Protects web applications from automated abuse using cryptographic proof-of-work challenges.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published