PAGI-FastAPI
view release on metacpan or search on metacpan
- eg/file_uploads_demo.pl
1.6.0 2026-08-25
[SECURITY FIXES]
- Added 'trust_proxies' configuration parameter to
PAGI::FastAPI::Middleware::BotProtection, defaults to 0.
- Added pow() reader method to PAGI::FastAPI::Middleware::BotProtection
to expose the underlying PAGI::FastAPI::BotProtection::ProofOfWork
instance for inspection and testing.
- Fixed IP spoofing security vulnerability by ignoring untrusted
'X-Forwarded-For' headers unless 'trust_proxies' is explicitly
enabled.
- Properly extract the original client IP from comma-separated
'X-Forwarded-For' proxy chains when 'trust_proxies' is enabled.
[TESTING]
- Added unit test: t/36-middleware_bot_protection_trust_proxies.t
1.5.0 2026-08-25
[SECURITY FIXES]
- CWE-1188: Removed default fallback string 'change_me_in_production'
from $secret in PAGI::FastAPI::BotProtection::ProofOfWork and
PAGI::FastAPI::Middleware::BotProtection.
- Enforced mandatory check for defined $secret; throws an exception
eg/rate_limit_demo.pl view on Meta::CPAN
use PAGI::FastAPI::RateLimit::Driver::Memory;
# A single key resolver, shared by every limiter below. It layers one
# demo-only override (an X-Demo-Client header) on top of the framework's
# own documented default chain, so a single browser tab can simulate
# multiple independent clients just by switching a dropdown, normally
# you'd rely on X-API-Key or the caller's IP alone.
my $key_cb = sub ($c) {
return $c->header('X-Demo-Client')
// $c->header('X-API-Key')
// $c->header('X-Forwarded-For')
// $c->scope->{client}[0]
// '127.0.0.1';
};
# Explicit driver instances (rather than letting add_rate_limit/rate_limit
# build their own) purely so /limits/reset below can reach in and clear a
# client's counters directly via reset_async(). Each limiter's counters
# are otherwise completely independent, driver-per-instance by default,
# see the note on /api/expensive.
my $app_driver = PAGI::FastAPI::RateLimit::Driver::Memory->new;
lib/PAGI/FastAPI/Middleware/BotProtection.pm view on Meta::CPAN
Challenges presented after this time window has elapsed will be rejected as
expired, requiring the client to request a fresh challenge.
=item * C<trust_proxies> (optional)
Boolean flag indicating whether to trust incoming proxy headers for client
IP resolution. Defaults to C<0> (false).
When set to C<0>, the middleware extracts the client IP strictly from the
direct TCP connection socket, ignoring client-supplied headers. When set to
C<1>, the middleware parses the first IP from the C<X-Forwarded-For> header
if present.
B<Security Note:> Only set C<trust_proxies> to C<1> when running behind a
trusted reverse proxy (such as NGINX, HAProxy, or AWS ALB) that strips or
overwrites incoming client header values.
=back
=head1 METHODS
lib/PAGI/FastAPI/Middleware/RateLimit.pm view on Meta::CPAN
field $trust_proxies :param = 0;
ADJUST {
$driver //= PAGI::FastAPI::RateLimit::Driver::Memory->new();
$key_cb //= sub ($c) {
# Rely on verified TCP peer socket by default
return $c->scope->{client}[0] // '127.0.0.1' unless $trust_proxies;
# Only inspect headers if explicit opt-in is enabled
return $c->header('X-API-Key')
// $c->header('X-Forwarded-For')
// $c->scope->{client}[0]
// '127.0.0.1';
};
}
async method handle ($c, $next) {
my $key = $key_cb->($c);
my ($count, $reset_at) = await $driver->increment_async($key, $window);
lib/PAGI/FastAPI/Middleware/RateLimit.pm view on Meta::CPAN
per window. Default: C<100>.
=item * C<window> - Optional integer. Duration of the rate-limiting window
in seconds. Default: C<60>.
=item * C<key_cb> - Optional C<CODE> reference accepting a
L<PAGI::FastAPI::Context> instance (C<$c>) and returning a unique scalar
string key identifying the client. By default, it falls back through:
1. C<X-API-Key> request header
2. C<X-Forwarded-For> request header
3. Client connection remote IP address (C<< $c->scope->{client}[0] >>)
4. Fallback default string C<'127.0.0.1'>
=item * C<driver> - Optional storage object implementing
C<increment_async($key, $window)>, C<get_async($key)> and C<reset_async($key)>.
Defaults to an instance of L<PAGI::FastAPI::RateLimit::Driver::Memory>.
=item * C<trust_proxies> - Optional boolean. When set to false (default),
the default identification key strictly uses the remote socket address
(C<< $c->scope->{client}[0] >>) to prevent header spoofing bypasses. Set to
true to allow fallback to C<X-API-Key> or C<X-Forwarded-For>.
=back
=head2 C<handle($c, $next)>
my $res = await $limiter->handle($c, $next);
Asynchronous method that executes the rate-limiting logic within the request
pipeline:
t/36-middleware_bot_protection_trust_proxies.t view on Meta::CPAN
subtest 'BotProtection client IP and trust_proxies behaviour' => sub {
my $secret = 'test-secret-key';
my $make_next = sub ($tracker_ref) {
return sub ($ctx) {
$$tracker_ref = 1;
return Future->done('OK');
};
};
# Test 1: trust_proxies => 0 ignores X-Forwarded-For and verifies via peer IP
{
my $mw = PAGI::FastAPI::Middleware::BotProtection->new(
secret => $secret,
trust_proxies => 0,
difficulty => 1,
);
my $peer_ip = '192.168.1.10';
my $spoofed_ip = '203.0.113.99';
t/36-middleware_bot_protection_trust_proxies.t view on Meta::CPAN
my $next_called = 0;
my $next = $make_next->(\$next_called);
$mw->handle($c, $next)->get;
is($next_called, 0, 'blocks request when challenge is bound to spoofed header');
is($c->status, 401, 'returns 401 Unauthorized status');
}
# Test 3: Explicit trust_proxies => 1 trusts X-Forwarded-For
{
my $mw = PAGI::FastAPI::Middleware::BotProtection->new(
secret => $secret,
trust_proxies => 1,
difficulty => 1,
);
my $peer_ip = '192.168.1.10';
my $proxy_ip = '203.0.113.50';
( run in 1.852 second using v1.01-cache-2.11-cpan-54e63673c56 )