NAME
Plack::Middleware::ProofOfWork - Proof-of-Work based bot protection for Plack applications
SYNOPSIS
use Plack::Builder;
builder {
enable "ProofOfWork",
difficulty => 4,
cookie_name => 'pow',
cookie_duration => 5;
$app;
};
DESCRIPTION
Plack::Middleware::ProofOfWork implements a Proof-of-Work mechanism to protect against automated requests (bots, scrapers, etc.). Legitimate browsers must solve a computationally intensive task before accessing the application.
The middleware uses SHA-256 hashing and requires clients to find a nonce that results in a hash with a specified number of leading zeros.
Features
- Bot Protection: Effective protection against automated requests and scraping
- Search Engine Friendly: Allows known search engine bots by default
- Browser-Based: Uses Web Crypto API for SHA-256 calculation in the browser
- Configurable: Adjustable difficulty and cookie settings
- User-Friendly: Modern UI with progress updates during calculation
- Fractional Difficulty: Fine-grained control with fractional values (e.g. 4.5)
- Customizable UI: External HTML template for complete control over challenge page
- Secure Logic: Validates PoW before bot check to prevent bypass attempts
CONFIGURATION
difficulty
The number of leading zeros in the hash (default: 4). Supports fractional values for finer granularity. Each additional zero increases difficulty by a factor of 16.
difficulty => 4 # ~65,000 attempts on average (~1-2 seconds)
difficulty => 4.5 # ~185,000 attempts (~3-5 seconds)
difficulty => 5 # ~1,000,000 attempts on average (~15-30 seconds)
Fractional difficulty enables finer gradations between the exponential steps of integer difficulties.
cookie_name
Name of the cookie for the Proof-of-Work token (default: 'pow').
cookie_duration
Cookie validity duration in days (default: 5).
bot_patterns
Hash-ref of bot types with DNS verification patterns.
bot_patterns => {
'googlebot/' => qr/crawl.*\.googlebot\.com$/,
'mybot' => qr/mybot.*example\.com$/,
}
Default includes 14 known bots:
googlebot/→crawl.*.googlebot.com$applebot/→applebot.apple.com$bingbot/→bingbot.*.bing.com$yandexbot/→.yandex.(ru|net|com)$petalbot→petalbot.*.petalsearch.com$ahrefsbot/→.ahrefs.(com|net)$barkrowler/→.babbar.eu$claudebot/,oai-searchbot/,gptbot/,mj12bot/,amazonbot/,perplexitybot/,duckduckbot/→^%$(no DNS verification)
The hash keys are used for case-insensitive User-Agent matching.
The regex values verify the reverse DNS hostname.
Pattern ^%$ means no DNS verification required (User-Agent only at level 1+).
bot_verification_level
Level of bot verification (default: 2):
- 0: Block all bots (no bots allowed, including search engines)
- 1: User-Agent only - simple string matching (fast but spoofable)
- 2: Reverse DNS - hostname must match pattern (good balance) - DEFAULT
- 3: Full DNS roundtrip - reverse + forward DNS must match (most secure)
bot_verification_level => 0 # Block all bots
bot_verification_level => 3 # Maximum security
Level 0 blocks all bots completely (useful for private/internal sites). Level 2 (default) provides good security with reasonable performance. Level 3 provides the strongest security against bot spoofing but adds forward DNS lookup latency.
bot_dns_timeout
Timeout in seconds for DNS lookups (default: 0.5).
bot_dns_timeout => 1.0 # Slower networks
Forward DNS lookup uses 2x this timeout. Increase for slower networks, decrease for faster response.
timestamp_window
Time window in seconds for timestamp rounding (default: cookie_duration * 86400).
js_file
Path to the JavaScript file for Proof-of-Work calculation.
js_file => '/path/to/my/pow.js'
If not specified, the bundled default file (share/pow.js) is used.
The file is loaded automatically from the installation via File::ShareDir.
This allows customization of the JavaScript for:
- Custom styling/branding
- Additional logging functionality
- Integration with monitoring tools
- Custom progress indicators
html_file
Path to the HTML template file for the challenge page.
html_file => '/path/to/my/challenge.html'
If not specified, the bundled default file (share/challenge.html) is used.
The HTML template must contain the placeholder <!-- POW_JAVASCRIPT -->
where the JavaScript API and pow.js content will be inserted.
This allows complete customization of:
- Page layout and design
- Branding and styling
- UI elements
- Status messages
css
Custom CSS to inject into the challenge page template.
css => '.spinner { border-color: #ff0000; }'
The CSS is inserted at the <!-- POW_CSS --> placeholder in the
HTML template's style section. This allows easy styling customization
without replacing the entire HTML template.
Example with branding colors:
enable "ProofOfWork",
css => q{
body {
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%);
}
.spinner {
border-top-color: #60a5fa;
}
};
LOGIC FLOW
The middleware checks Proof-of-Work in the following order:
- Check if PoW cookie exists
- If cookie exists: Validate it
- Valid → Allow request through
- Invalid → Require new PoW (even for bots)
- If no cookie: Check if request is from a verified bot
- Is verified bot (level 1-3) → Allow through
- Not a bot or level 0 → Require PoW
This ensures that even bots with invalid cookies must solve the PoW again.
BOT VERIFICATION
Version 0.13+ includes reliable bot verification using DNS checks to prevent User-Agent spoofing.
Verification Levels
Level 0: Block All Bots
enable "ProofOfWork",
bot_verification_level => 0; # All bots blocked (including search engines)
Blocks all bots completely. Useful for private or internal sites.
Level 1: User-Agent Only (Default pre-0.13)
enable "ProofOfWork",
bot_verification_level => 1; # Fast but spoofable
Checks if User-Agent contains bot name. Easy to spoof.
Level 2: Reverse DNS (Default)
enable "ProofOfWork",
bot_verification_level => 2; # Good balance (default)
- Check User-Agent contains bot name
- Reverse DNS lookup: IP → hostname
- Verify hostname matches pattern (e.g.,
*.google.com)
Prevents simple spoofing. Good balance of security and performance.
Level 3: Full DNS Roundtrip
enable "ProofOfWork",
bot_verification_level => 3; # Most secure
- Check User-Agent contains bot name
- Reverse DNS: IP → hostname
- Verify hostname matches pattern
- Forward DNS: hostname → IP addresses
- Verify original IP is in resolved addresses
Most secure - prevents both User-Agent spoofing and DNS cache poisoning.
Custom Bot Patterns
enable "ProofOfWork",
bot_patterns => {
'googlebot' => qr/crawl.*google\.com$/,
'mybot' => qr/mybot.*mycompany\.com$/,
},
bot_verification_level => 3;
Pattern must match the reverse DNS hostname.
HOW IT WORKS
-
Initial Request: A client without a valid PoW cookie receives an HTML page with JavaScript.
-
Challenge: The JavaScript calculates a Proof-of-Work based on:
- User-Agent
- Accept-Language header
- Host
- Current timestamp (rounded to cookie window)
-
Solution: The client finds a nonce that, together with the above values, produces a hash with the desired number of leading zeros.
-
Cookie: After successful calculation, a cookie is set and the page reloads.
-
Access: The middleware validates the cookie and passes subsequent requests through.
EXAMPLES
Simple Usage
use Plack::Builder;
my $app = sub {
return [200, ['Content-Type' => 'text/plain'], ['Hello World']];
};
builder {
enable "ProofOfWork";
$app;
};
With Custom Configuration
builder {
enable "ProofOfWork",
difficulty => 5, # Higher difficulty
cookie_name => 'bot_check',
cookie_duration => 7, # One week
bot_verification_level => 2, # Reverse DNS (default)
bot_patterns => { # Custom bot list
'googlebot' => qr/crawl.*google\.com$/,
'mylegitbot' => qr/mybot.*example\.com$/,
};
$app;
};
With Custom JavaScript File
builder {
enable "ProofOfWork",
difficulty => 4,
js_file => '/var/www/myapp/custom-pow.js'; # Custom JS file
$app;
};
The JavaScript file must provide:
sha256(message)- SHA-256 hash calculationcomputeProofOfWork()- Main PoW functionsetCookie(name, value, days)- Cookie setter
And use these constants:
DIFFICULTY- Difficulty (set by Perl)POW_COOKIE_NAME- Cookie name (set by Perl)COOKIE_DURATION- Cookie duration (set by Perl)getSourceValue()- Function providing source value (set by Perl)
With Custom HTML Template
builder {
enable "ProofOfWork",
difficulty => 4,
html_file => '/var/www/myapp/challenge.html'; # Custom HTML
$app;
};
The HTML template must include the placeholder:
<!-- POW_JAVASCRIPT -->
This will be replaced with the JavaScript API and pow.js content.
Example custom template:
<!DOCTYPE html>
<html>
<head>
<title>Please Wait...</title>
<style>/* Your custom styles */</style>
</head>
<body>
<div class="your-custom-ui">
<img src="/logo.png" alt="Logo">
<h1>Verifying your browser...</h1>
<div id="status"></div>
</div>
<script>
<!-- POW_JAVASCRIPT -->
</script>
</body>
</html>
With Custom CSS
builder {
enable "ProofOfWork",
difficulty => 4,
css => q{
body {
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 100%);
}
.spinner {
border-color: rgba(255, 255, 255, 0.2);
border-top-color: #60a5fa;
}
h1 {
color: #dbeafe;
}
};
$app;
};
The custom CSS is injected at the <!-- POW_CSS --> placeholder in the template.
Only Protect Specific Paths
builder {
mount "/api" => builder {
enable "ProofOfWork", difficulty => 5;
$api_app;
};
mount "/" => $public_app;
};
Combine With Other Middleware
builder {
enable "AccessLog", format => "combined";
enable "ProofOfWork", difficulty => 4;
enable "Session", store => "File";
$app;
};
CHOOSING DIFFICULTY
The right difficulty depends on your requirements:
| Difficulty | Attempts (Avg) | Time (Avg) | Use Case | |------------|----------------|-------------|---------------------------------| | 2 | ~256 | <0.1s | Testing only | | 3 | ~4,000 | ~0.2s | Very light protection | | 4 | ~65,000 | ~1-2s | Recommended for most cases | | 4.5 | ~185,000 | ~3-5s | Stronger protection, acceptable | | 5 | ~1,000,000 | ~15-30s | High protection, may frustrate | | 5.5 | ~2,800,000 | ~45-90s | Very high protection | | 6 | ~16,000,000 | ~4-8 min | Extreme protection, special cases only |
Recommendation: Start with difficulty 4 and adjust as needed.
SECURITY CONSIDERATIONS
-
Difficulty: Should be high enough to slow down automated requests, but low enough not to frustrate legitimate users. Difficulty 4-5 is appropriate for most applications.
-
Token Reuse: The middleware uses User-Agent, Accept-Language, and Host as part of the challenge to prevent token reuse across different contexts.
-
Timestamp Rounding: Timestamps are rounded to the cookie window to ensure stable challenges and cache-friendliness.
-
Not a Silver Bullet: Proof-of-Work is not a perfect solution against all bots. Motivated attackers can solve the challenge, but it significantly increases costs.
PERFORMANCE
Server-side performance impact is minimal:
- Without valid cookie: One SHA-256 hash calculation for validation
- With valid cookie: One SHA-256 hash calculation for validation
- Computational effort: Completely on the client (browser)
Typical client-side calculation times:
| Difficulty | Average Time | Attempts | |------------|--------------|---------------------| | 3 | ~0.1s | ~4,000 | | 4 | ~1-2s | ~65,000 | | 4.5 | ~3-5s | ~185,000 | | 5 | ~15-30s | ~1,000,000 | | 5.5 | ~45-90s | ~2,800,000 | | 6 | ~4-8 min | ~16,000,000 |
DEPENDENCIES
- Plack >= 1.0000
- Digest::SHA
- MIME::Base64
- File::ShareDir
- Modern browser with Web Crypto API support
SEE ALSO
AUTHOR
Oliver Paukstadt
LICENSE
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
COPYRIGHT
Copyright (C) 2026 Oliver Paukstadt