PAGI-FastAPI

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

         - 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 )