Punk
view release on metacpan or search on metacpan
include/punk/punk_proxy.h view on Meta::CPAN
* address without any of them being changed. The mount path already rewrites
* env keys in place (punk_serve.h), so this is an established move.
*
* Why it exists: rate_limit keys on REMOTE_ADDR, and because Hyperman's
* arena makes a counter exact across the whole worker pool rather than per
* worker, a limiter behind a proxy puts EVERY client in one bucket - a
* 100/min rule throttles the whole site at 100/min. block_ip, keyed the same
* way, bans the load balancer.
*
* The hop rule, which is what almost every implementation gets wrong:
* X-Forwarded-For reads `client, proxy1, proxy2` and each hop APPENDS the
* address it received the connection FROM. The socket peer is the last
* proxy and never appears in the header it forwarded. So with N trusted
* proxies the client sits at index N-1 counting from the RIGHT. Taking the
* leftmost entry is the spoofable version: the client writes that one.
*
* Everything here is prefixed pp_ / PP_. Needs nothing but punk_compat.h;
* included before punk_serve.h, which calls pp_resolve.
*/
#include <string.h>
include/punk/punk_proxy.h view on Meta::CPAN
p->ncidrs = (int)n;
p->hops = 0;
}
else if (!SvROK(*t) && !looks_like_number(*t)) {
STRLEN sl;
const char *s = SvPV_const(*t, sl);
if (sl == 3 && memEQ(s, "all", 3)) {
if (!(appenv && strEQ(appenv, "development"))) {
pp_free(aTHX_ p);
croak("Punk: proxy: trust => 'all' believes any "
"X-Forwarded-For from anyone, so with no proxy in "
"front it is a total bypass - it is allowed only "
"under PUNK_ENV=development. Name a hop count or "
"the proxy networks instead");
}
p->hops = -1;
} else {
SV *msg = sv_2mortal(newSVpvf(
"Punk: proxy: trust must be a hop count, an arrayref of "
"CIDRs, or 'all' - not '%.*s'", (int)sl, s));
pp_free(aTHX_ p);
include/punk/punk_proxy.h view on Meta::CPAN
if (n < 1 || n > PP_MAX_HOPS) {
pp_free(aTHX_ p);
croak("Punk: proxy: trust => %" IVdf " is not a usable hop "
"count (1..%d)", n, PP_MAX_HOPS);
}
p->hops = (int)n;
}
}
p->for_key = pp_key_opt(aTHX_ cfg, "for_header",
"X-Forwarded-For", &p->for_len);
p->proto_key = pp_key_opt(aTHX_ cfg, "proto_header",
"X-Forwarded-Proto", &p->proto_len);
p->host_key = pp_key_opt(aTHX_ cfg, "host_header",
"X-Forwarded-Host", &p->host_len);
p->port_key = pp_key_opt(aTHX_ cfg, "port_header",
"X-Forwarded-Port", &p->port_len);
return p;
}
/* ---- the per-request resolution ----------------------------------------- */
lib/Punk.pm view on Meta::CPAN
C<X-Forwarded-Proto> sets C<psgi.url_scheme> (and C<HTTPS>),
C<X-Forwarded-Host> sets C<HTTP_HOST>, and C<X-Forwarded-Port> sets
C<SERVER_PORT>, all under the same trust decision.
B<Without this keyword, a limiter behind a proxy is not just approximate -
it is a site-wide outage waiting to happen.> See L</The shared bucket>
below.
=head3 How C<trust> counts
C<X-Forwarded-For> reads C<< client, proxy1, proxy2 >>, and each hop
B<appends> the address it received the connection I<from>. The socket peer
is the last proxy and never appears in the header it forwarded. So with
C<< trust => N >> the client sits at index C<N-1> counting from the
B<right>.
Counting from the left is the spoofable version, because the leftmost entry
is the one the client writes. With one proxy in front and a client sending
C<< X-Forwarded-For: 9.9.9.9 >>, the header arriving here is
C<< 9.9.9.9, <real client> >> - and Punk answers with the real client.
A chain shorter than C<trust> declares is a misconfiguration, or a client
that sent nothing; the answer is then the socket peer, never the leftmost
entry. An entry that is not a valid address ends the walk the same way -
C<REMOTE_ADDR> feeds a shared-memory rate-limit key, so attacker-controlled
bytes must never reach it.
C<< trust => \@cidrs >> walks right to left while each entry is one of the
named networks and takes the first one that is not, having first checked
lib/Punk.pm view on Meta::CPAN
=head3 The shared bucket
C<rate_limit> keys on C<REMOTE_ADDR>, and because the counters live in
L<Hyperman>'s shared arena a limit is B<exact across the whole worker pool>
rather than per worker. Behind a proxy without this keyword, C<REMOTE_ADDR>
is the proxy for every request, so every client on the internet shares one
bucket and a C<< limit => 100 >> rule throttles the entire site at 100 per
window. C<< $c->block_ip >>, keyed the same way, bans the load balancer.
Reaching for C<< by => 'header:X-Forwarded-For' >> instead is worse, not
better: nothing validates the header, so on an application that is I<not>
behind a proxy any client can set it and step into a fresh bucket at will.
=head3 What this does not fix
L<Hyperman>'s edge denylist drops a connection at C<accept>, before a byte
is read, so it cannot see a header and never will. Behind a proxy it can
only ever match the proxy's own address. C<< $c->block_ip($client) >> still
writes to the arena, but the ban takes effect at dispatch as a C<403>
rather than at the edge - the same outcome, at the cost of a request.
lib/Punk/RateLimit.pm view on Meta::CPAN
proxy is the I<proxy's> address on every request, and because the counter is
shared across the whole worker pool rather than per worker, every client on
the internet lands in B<one bucket>: a C<< limit =E<gt> 100 >> rule then
throttles the entire site at 100 per window, and C<< $c-E<gt>block_ip >>
bans the load balancer.
use Punk;
proxy; # one proxy in front
rate_limit limit => 100, window => 60; # now keyed on the real client
Do not reach for C<< by =E<gt> 'header:X-Forwarded-For' >> instead. Nothing
validates that header, so on an application that is not actually behind a
proxy any client can set it and step into a fresh bucket at will - a bypass
in place of a shared bucket. L<Punk/proxy> validates the hop chain; this
does not.
Note also that once a proxy is in front, C<< $c-E<gt>block_ip >> can no
longer be enforced at C<accept>: the edge sees only the proxy, so the ban
becomes a C<403> at dispatch. Same outcome, one request's worth of cost.
=head1 SEE ALSO
t/1210-proxy-parse.t view on Meta::CPAN
use 5.010;
use strict;
use warnings;
use Test::More;
use Punk ();
# The hop walk alone (pp_xff_client, punk_proxy.h) through the Punk::Proxy
# author shim. This is the part every implementation gets wrong, so it is
# tested as a table before anything is wired to a request.
#
# The rule: X-Forwarded-For reads `client, proxy1, proxy2` and each hop
# APPENDS the address it received FROM. The socket peer is the last proxy and
# never appears in the header it forwarded. With N trusted proxies the client
# is at index N-1 counting from the RIGHT. Reaching for the leftmost entry is
# the spoofable version, because the client writes that one.
sub client { Punk::Proxy::_client($_[0], $_[1], $_[2]) }
# ---- fixed hop counts ------------------------------------------------------
is client('1.2.3.4', '10.0.0.1', 1), '1.2.3.4',
'one proxy, one entry: that entry is the client';
is client('1.2.3.4, 10.0.0.7', '10.0.0.1', 2), '1.2.3.4',
'two proxies, two entries: the leftmost is the client';
is client('1.2.3.4, 10.0.0.7, 10.0.0.8', '10.0.0.1', 3), '1.2.3.4',
'three proxies, three entries';
# THE SPOOF CASE. One real proxy in front. The client sends its own
# X-Forwarded-For claiming to be 9.9.9.9; the proxy appends the address it
# actually saw. Counting from the right lands on the truth; counting from the
# left would hand the attacker any address it liked - including one that is
# on somebody else's rate-limit bucket, or off a denylist.
is client('9.9.9.9, 1.2.3.4', '10.0.0.1', 1), '1.2.3.4',
'a forged leading entry is ignored: the hop count counts from the right';
is client('7.7.7.7, 9.9.9.9, 1.2.3.4', '10.0.0.1', 1), '1.2.3.4',
'a whole forged chain is ignored';
is client('9.9.9.9, 1.2.3.4, 10.0.0.7', '10.0.0.1', 2), '1.2.3.4',
t/1211-proxy.t view on Meta::CPAN
use Punk;
proxy trust => 1, for_header => 'CF-Connecting-IP';
get '/env' => \&main::echo;
}
{
my $call = caller_for(Custom->to_app);
my $d = body_of($call->(hdr => 'CF_CONNECTING_IP', hval => '1.2.3.4',
xff => '9.9.9.9'));
is $d->{remote_addr}, '1.2.3.4', 'for_header names a different header';
isnt $d->{remote_addr}, '9.9.9.9',
'and the standard X-Forwarded-For is not consulted once it is renamed';
}
# ---- CIDR trust ------------------------------------------------------------
{
package Cidr;
use Punk;
proxy trust => ['10.0.0.0/8'];
get '/env' => \&main::echo;
}
( run in 1.081 second using v1.01-cache-2.11-cpan-54e63673c56 )