diff --git a/Changes b/Changes index bcbd714..82676eb 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,14 @@ Release notes for HTTP-Tiny {{$NEXT}} + [!!! SECURITY !!!] + + - Caller-supplied C, C, and C + headers are now stripped on cross-origin redirects by default. Use + allow_credentialed_redirects to opt out. + + - Redirects are no longer automatically followed when going from https to http. + Use allow_downgrade to revert to the original behaviour. 0.093 2026-05-11 17:18:12+02:00 Europe/Brussels (TRIAL RELEASE) diff --git a/corpus/redirect-11.txt b/corpus/redirect-11.txt new file mode 100644 index 0000000..6ecdf4d --- /dev/null +++ b/corpus/redirect-11.txt @@ -0,0 +1,21 @@ +url + https://victim.example/secret +expected + refused-redirect-body +expected_url + https://victim.example/secret +---------- +GET /secret HTTP/1.1 +Host: victim.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 21 +Location: http://victim.example/secret + +refused-redirect-body + diff --git a/corpus/redirect-12.txt b/corpus/redirect-12.txt new file mode 100644 index 0000000..8509b24 --- /dev/null +++ b/corpus/redirect-12.txt @@ -0,0 +1,36 @@ +url + https://victim.example/secret +expected + success +expected_url + http://victim.example/secret +new_args + allow_downgrade: 1 +---------- +GET /secret HTTP/1.1 +Host: victim.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: http://victim.example/secret + +redirect + +---------- +GET /secret HTTP/1.1 +Host: victim.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 7 + +success diff --git a/corpus/redirect-13.txt b/corpus/redirect-13.txt new file mode 100644 index 0000000..6d22dc9 --- /dev/null +++ b/corpus/redirect-13.txt @@ -0,0 +1,35 @@ +url + https://example.com/index.html +expected + abcdefghijklmnopqrstuvwxyz1234567890abcdef +expected_url + https://example.com/index2.html +---------- +GET /index.html HTTP/1.1 +Host: example.com +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/html +Content-Length: 53 +Location: https://example.com/index2.html + +redirect + +---------- +GET /index2.html HTTP/1.1 +Host: example.com +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 42 + +abcdefghijklmnopqrstuvwxyz1234567890abcdef + diff --git a/corpus/redirect-14.txt b/corpus/redirect-14.txt new file mode 100644 index 0000000..31e81f3 --- /dev/null +++ b/corpus/redirect-14.txt @@ -0,0 +1,35 @@ +url + http://example.com/index.html +expected + abcdefghijklmnopqrstuvwxyz1234567890abcdef +expected_url + https://example.com/index2.html +---------- +GET /index.html HTTP/1.1 +Host: example.com +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/html +Content-Length: 53 +Location: https://example.com/index2.html + +redirect + +---------- +GET /index2.html HTTP/1.1 +Host: example.com +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 42 + +abcdefghijklmnopqrstuvwxyz1234567890abcdef + diff --git a/corpus/redirect-15.txt b/corpus/redirect-15.txt new file mode 100644 index 0000000..35eb003 --- /dev/null +++ b/corpus/redirect-15.txt @@ -0,0 +1,57 @@ +url + http://victim.example/secret +expected + pwned +expected_url + http://victim.example/back +headers + Authorization: Bearer SECRET-TOKEN + Cookie: session=SECRET-SESSION + Proxy-Authorization: Basic c2VjcmV0OnNlY3JldA== +---------- +GET /secret HTTP/1.1 +Host: victim.example +Authorization: Bearer SECRET-TOKEN +Cookie: session=SECRET-SESSION +Proxy-Authorization: Basic c2VjcmV0OnNlY3JldA== +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: http://attacker.example/loot + +redirect + +---------- +GET /loot HTTP/1.1 +Host: attacker.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: http://victim.example/back + +redirect + +---------- +GET /back HTTP/1.1 +Host: victim.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 5 + +pwned + diff --git a/corpus/redirect-16.txt b/corpus/redirect-16.txt new file mode 100644 index 0000000..986a95d --- /dev/null +++ b/corpus/redirect-16.txt @@ -0,0 +1,47 @@ +url + http://victim.example/secret +expected + pwned +expected_url + http://attacker.example/loot +new_args + allow_credentialed_redirects: 1 +headers + Authorization: Bearer SECRET-TOKEN + Cookie: session=SECRET-SESSION + Proxy-Authorization: Basic c2VjcmV0OnNlY3JldA== +---------- +GET /secret HTTP/1.1 +Host: victim.example +Authorization: Bearer SECRET-TOKEN +Cookie: session=SECRET-SESSION +Proxy-Authorization: Basic c2VjcmV0OnNlY3JldA== +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: http://attacker.example/loot + +redirect + +---------- +GET /loot HTTP/1.1 +Host: attacker.example +Authorization: Bearer SECRET-TOKEN +Cookie: session=SECRET-SESSION +Proxy-Authorization: Basic c2VjcmV0OnNlY3JldA== +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 5 + +pwned + diff --git a/corpus/redirect-17.txt b/corpus/redirect-17.txt new file mode 100644 index 0000000..141dd6c --- /dev/null +++ b/corpus/redirect-17.txt @@ -0,0 +1,39 @@ +url + http://example.com/a +expected + ok +expected_url + http://example.com/b +headers + Authorization: Bearer SECRET-TOKEN +---------- +GET /a HTTP/1.1 +Host: example.com +Authorization: Bearer SECRET-TOKEN +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: http://example.com/b + +redirect + +---------- +GET /b HTTP/1.1 +Host: example.com +Authorization: Bearer SECRET-TOKEN +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 2 + +ok + diff --git a/corpus/redirect-18.txt b/corpus/redirect-18.txt new file mode 100644 index 0000000..09e8073 --- /dev/null +++ b/corpus/redirect-18.txt @@ -0,0 +1,38 @@ +url + http://example.com:8080/foo +expected + ok +expected_url + http://example.com:8081/bar +headers + Authorization: Bearer SECRET-TOKEN +---------- +GET /foo HTTP/1.1 +Host: example.com:8080 +Authorization: Bearer SECRET-TOKEN +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: http://example.com:8081/bar + +redirect + +---------- +GET /bar HTTP/1.1 +Host: example.com:8081 +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 2 + +ok + diff --git a/corpus/redirect-19.txt b/corpus/redirect-19.txt new file mode 100644 index 0000000..2b52d20 --- /dev/null +++ b/corpus/redirect-19.txt @@ -0,0 +1,40 @@ +url + https://example.com:8443/foo +expected + ok +expected_url + http://example.com:8443/foo +new_args + allow_downgrade: 1 +headers + Authorization: Bearer SECRET-TOKEN +---------- +GET /foo HTTP/1.1 +Host: example.com:8443 +Authorization: Bearer SECRET-TOKEN +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: http://example.com:8443/foo + +redirect + +---------- +GET /foo HTTP/1.1 +Host: example.com:8443 +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 2 + +ok + diff --git a/corpus/redirect-20.txt b/corpus/redirect-20.txt new file mode 100644 index 0000000..9553352 --- /dev/null +++ b/corpus/redirect-20.txt @@ -0,0 +1,41 @@ +url + http://victim.example/submit +method + POST +expected + ok +expected_url + http://attacker.example/loot +headers + Authorization: Bearer SECRET-TOKEN +---------- +POST /submit HTTP/1.1 +Host: victim.example +Authorization: Bearer SECRET-TOKEN +Connection: close +Content-Length: 0 +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 303 See Other +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: http://attacker.example/loot + +redirect + +---------- +GET /loot HTTP/1.1 +Host: attacker.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 2 + +ok + diff --git a/corpus/redirect-21.txt b/corpus/redirect-21.txt new file mode 100644 index 0000000..8670368 --- /dev/null +++ b/corpus/redirect-21.txt @@ -0,0 +1,38 @@ +url + https://victim.example/x +expected + pwned +expected_url + https://attacker.example/loot +headers + Authorization: Bearer TRUSTED-TOKEN +---------- +GET /x HTTP/1.1 +Host: victim.example +Authorization: Bearer TRUSTED-TOKEN +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: //attacker.example/loot + +redirect + +---------- +GET /loot HTTP/1.1 +Host: attacker.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 5 + +pwned + diff --git a/corpus/redirect-22.txt b/corpus/redirect-22.txt new file mode 100644 index 0000000..c5534a6 --- /dev/null +++ b/corpus/redirect-22.txt @@ -0,0 +1,40 @@ +url + http://example.com/login +expected + ok +expected_url + https://example.com/login +headers + Authorization: Bearer SECRET-TOKEN + Cookie: session=SECRET-SESSION +---------- +GET /login HTTP/1.1 +Host: example.com +Authorization: Bearer SECRET-TOKEN +Cookie: session=SECRET-SESSION +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: https://example.com/login + +redirect + +---------- +GET /login HTTP/1.1 +Host: example.com +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 2 + +ok + diff --git a/corpus/redirect-23.txt b/corpus/redirect-23.txt new file mode 100644 index 0000000..de44874 --- /dev/null +++ b/corpus/redirect-23.txt @@ -0,0 +1,36 @@ +url + https://user:pass@victim.example/secret +expected + ok +expected_url + https://attacker.example/loot +---------- +GET /secret HTTP/1.1 +Host: victim.example +Connection: close +User-Agent: HTTP-Tiny/VERSION +Authorization: Basic dXNlcjpwYXNz + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: https://attacker.example/loot + +redirect + +---------- +GET /loot HTTP/1.1 +Host: attacker.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 2 + +ok + diff --git a/corpus/redirect-24.txt b/corpus/redirect-24.txt new file mode 100644 index 0000000..c203e97 --- /dev/null +++ b/corpus/redirect-24.txt @@ -0,0 +1,38 @@ +url + https://user:pass@victim.example/secret +expected + ok +expected_url + https://attacker.example/loot +new_args + allow_credentialed_redirects: 1 +---------- +GET /secret HTTP/1.1 +Host: victim.example +Connection: close +User-Agent: HTTP-Tiny/VERSION +Authorization: Basic dXNlcjpwYXNz + +---------- +HTTP/1.1 302 Found +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 8 +Location: https://attacker.example/loot + +redirect + +---------- +GET /loot HTTP/1.1 +Host: attacker.example +Connection: close +User-Agent: HTTP-Tiny/VERSION + +---------- +HTTP/1.1 200 OK +Date: Thu, 03 Feb 1994 00:00:00 GMT +Content-Type: text/plain +Content-Length: 2 + +ok + diff --git a/lib/HTTP/Tiny.pm b/lib/HTTP/Tiny.pm index ff6241e..3dc828b 100644 --- a/lib/HTTP/Tiny.pm +++ b/lib/HTTP/Tiny.pm @@ -18,6 +18,16 @@ This constructor returns a new HTTP::Tiny object. Valid attributes include: * C — A user-agent string (defaults to 'HTTP-Tiny/$VERSION'). If C — ends in a space character, the default user-agent string is appended. +* C - If a 3xx redirects to a different scheme, + host or port, by default HTTP::Tiny will strip away caller-supplied + C, C and C headers from the + redirected request and from all subsequent requests in the chain. Set this to a + true value to revert to the legacy behavior of forwarding those headers. + Default is C. +* C — If a 3xx redirect changes the scheme from C to + plain C, HTTP::Tiny will by default refuse to follow it, returning the + 3xx response. Set this to a true value to revert to the legacy behavior of + redirecting C to C. Default is C. * C — An instance of L — or equivalent class that supports the C and C methods * C — A hashref of default headers to apply to requests @@ -81,9 +91,9 @@ attributes. my @attributes; BEGIN { @attributes = qw( - cookie_jar default_headers http_proxy https_proxy keep_alive - local_address max_redirect max_size proxy no_proxy - SSL_options verify_SSL + allow_credentialed_redirects allow_downgrade cookie_jar default_headers + http_proxy https_proxy keep_alive local_address max_redirect max_size + proxy no_proxy SSL_options verify_SSL ); my %persist_ok = map {; $_ => 1 } qw( cookie_jar default_headers max_redirect max_size @@ -364,8 +374,7 @@ Don't use C when you really want C. See L for how this applies to redirection. If the URL includes a "user:password" stanza, they will be used for Basic-style -authorization headers. (Authorization headers will not be included in a -redirected request.) For example: +authorization headers. For example: $http->request('GET', 'http://Aladdin:open sesame@example.com/'); @@ -374,6 +383,10 @@ be percent-escaped: $http->request('GET', 'http://john%40example.com:password@example.com/'); +Caller-supplied C, C and C headers +are stripped on cross-origin redirects. See L's +C attribute to opt out. + A hashref of options may be appended to modify the request. Valid options are: @@ -458,6 +471,7 @@ contain 599, and the C field will contain the text of the error. =cut my %idempotent = map { $_ => 1 } qw/GET HEAD PUT DELETE OPTIONS TRACE/; +my %sensitive_headers = map { $_ => 1 } qw/authorization cookie proxy-authorization/; sub request { my ($self, $method, $url, $args) = @_; @@ -842,6 +856,7 @@ sub _prepare_headers_and_cb { for ($self->{default_headers}, $args->{headers}) { next unless defined; while (my ($k, $v) = each %$_) { + next if $args->{_strip_credentials} && exists $sensitive_headers{lc $k}; $request->{headers}{lc $k} = $v; $request->{header_case}{lc $k} = $k; } @@ -969,9 +984,24 @@ sub _maybe_redirect { and $headers->{location} and @{$args->{_redirects}} < $self->{max_redirect} ) { - my $location = ($headers->{location} =~ /^\//) + my $location = $headers->{location} =~ m{^//} + ? "$request->{scheme}:$headers->{location}" + : $headers->{location} =~ m{^/} ? "$request->{scheme}://$request->{host_port}$headers->{location}" - : $headers->{location} ; + : $headers->{location}; + my ($to_scheme, $to_host, $to_port) = $self->_split_url($location); + if (!$self->{allow_downgrade} && $request->{scheme} eq 'https' && $to_scheme eq 'http' ) { + return; + } + if ( + !$self->{allow_credentialed_redirects} + && ( $request->{scheme} ne $to_scheme + || $request->{host} ne $to_host + || $request->{port} ne $to_port ) + ) { + $args->{_strip_credentials} = 1; + } + return (($status eq '303' ? 'GET' : $method), $location); } return; @@ -1767,6 +1797,8 @@ sub _ssl_args { =for Pod::Coverage SSL_options agent +allow_credentialed_redirects +allow_downgrade cookie_jar default_headers http_proxy diff --git a/t/001_api.t b/t/001_api.t index 48cc2f8..403fff7 100755 --- a/t/001_api.t +++ b/t/001_api.t @@ -7,8 +7,9 @@ use Test::More tests => 2; use HTTP::Tiny; my @accessors = qw( - agent default_headers http_proxy https_proxy keep_alive local_address - max_redirect max_size proxy no_proxy timeout SSL_options verify_SSL cookie_jar + agent allow_credentialed_redirects allow_downgrade default_headers http_proxy + https_proxy keep_alive local_address max_redirect max_size proxy no_proxy timeout + SSL_options verify_SSL cookie_jar ); my @methods = qw( new get head put post patch delete post_form request mirror www_form_urlencode can_ssl