Punk
view release on metacpan or search on metacpan
t/1210-proxy-parse.t view on Meta::CPAN
#!perl
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',
'forged entry with two real proxies';
# A chain SHORTER than declared is a misconfiguration or a client that sent
# nothing. The peer is the honest answer; the leftmost entry is not.
is client('1.2.3.4', '10.0.0.1', 2), '10.0.0.1',
'fewer entries than declared hops: fall back to the socket peer';
is client('1.2.3.4, 10.0.0.7', '10.0.0.1', 5), '10.0.0.1',
'far fewer entries than declared hops: the peer';
# ---- CIDR trust ------------------------------------------------------------
is client('1.2.3.4, 10.0.0.7', '10.0.0.1', ['10.0.0.0/8']), '1.2.3.4',
'walk right to left, stop at the first untrusted entry';
is client('9.9.9.9, 1.2.3.4, 10.0.0.7', '10.0.0.1', ['10.0.0.0/8']), '1.2.3.4',
'the walk stops at the first untrusted entry, forged ones beyond it unread';
is client('10.0.0.5, 10.0.0.7', '10.0.0.1', ['10.0.0.0/8']), '10.0.0.5',
'an entirely trusted chain yields the leftmost entry';
is client('1.2.3.4, 10.0.0.7', '8.8.8.8', ['10.0.0.0/8']), '8.8.8.8',
'an untrusted socket peer means the header is not ours to believe';
is client('1.2.3.4, 172.16.0.9', '10.0.0.1',
['10.0.0.0/8', '172.16.0.0/12']), '1.2.3.4',
'several trusted networks';
is client('1.2.3.4, 10.0.0.7', '10.0.0.1', ['10.0.0.1']), '10.0.0.7',
'a bare address in the trust list is a /32 host: 10.0.0.7 is not it';
# ---- trust => all ----------------------------------------------------------
is client('9.9.9.9, 1.2.3.4', '10.0.0.1', 'all'), '9.9.9.9',
"trust => 'all' takes the leftmost entry, forged or not";
is client('', '10.0.0.1', 'all'), '10.0.0.1',
"trust => 'all' with an empty header: the peer";
# ---- IPv6 ------------------------------------------------------------------
is client('2001:db8::1, fd00::1', 'fd00::2', ['fd00::/8']), '2001:db8::1',
'v6 chain and a v6 CIDR';
is client('::ffff:1.2.3.4, 10.0.0.7', '10.0.0.1', ['10.0.0.0/8']),
'::ffff:1.2.3.4',
'a v4-mapped v6 entry survives the walk';
# A v4-mapped v6 address must match the v4 CIDR an operator wrote: that is
# what a dual-stack listener hands you, and treating it as v6 would silently
( run in 0.714 second using v1.01-cache-2.11-cpan-54e63673c56 )