|
| 1 | +Description: Strip Authorization and Proxy-Authorization on cross-origin redirect |
| 2 | + Prevent credential leakage by stripping Authorization and Proxy-Authorization |
| 3 | + headers when following cross-origin 3xx redirects (different scheme, host, or |
| 4 | + port). Also refuse https->http downgrade redirects by default. |
| 5 | + . |
| 6 | + Same defect class as libcurl CVE-2018-1000007, Python requests CVE-2018-18074, |
| 7 | + wget CVE-2021-31879, and Go net/http CVE-2024-45336. |
| 8 | + . |
| 9 | + LWP::UserAgent (libwww-perl) |
| 10 | + . |
| 11 | + CVE-2026-8368 |
| 12 | +Author: Olaf Alders <olaf@wundersolutions.com> |
| 13 | +Origin: upstream, https://github.com/libwww-perl/libwww-perl/commit/9c4aeb6 |
| 14 | +Origin: upstream, https://github.com/libwww-perl/libwww-perl/commit/792a5dc |
| 15 | +Bug: https://github.com/libwww-perl/libwww-perl/issues |
| 16 | +Forwarded: not-needed |
| 17 | +Last-Update: 2026-06-04 |
| 18 | + |
| 19 | +Index: libwww-perl/lib/LWP/UserAgent.pm |
| 20 | +=================================================================== |
| 21 | +--- libwww-perl.orig/lib/LWP/UserAgent.pm |
| 22 | ++++ libwww-perl/lib/LWP/UserAgent.pm |
| 23 | +@@ -85,6 +85,9 @@ sub new |
| 24 | + $requests_redirectable = ['GET', 'HEAD'] |
| 25 | + unless defined $requests_redirectable; |
| 26 | + |
| 27 | ++ my $allow_credentialed_redirects = delete $cnf{allow_credentialed_redirects}; |
| 28 | ++ my $allow_downgrade = delete $cnf{allow_downgrade}; |
| 29 | ++ |
| 30 | + # Actually ""s are just as good as 0's, but for concision we'll just say: |
| 31 | + Carp::croak("protocols_allowed has to be an arrayref or 0, not \"$protocols_allowed\"!") |
| 32 | + if $protocols_allowed and ref($protocols_allowed) ne 'ARRAY'; |
| 33 | +@@ -111,6 +114,8 @@ sub new |
| 34 | + no_proxy => [ @{ $no_proxy } ], |
| 35 | + protocols_allowed => $protocols_allowed, |
| 36 | + protocols_forbidden => $protocols_forbidden, |
| 37 | ++ allow_credentialed_redirects => $allow_credentialed_redirects, |
| 38 | ++ allow_downgrade => $allow_downgrade, |
| 39 | + requests_redirectable => $requests_redirectable, |
| 40 | + send_te => $send_te, |
| 41 | + }, $class; |
| 42 | +@@ -360,6 +365,42 @@ sub request { |
| 43 | + } |
| 44 | + $referral->uri($referral_uri); |
| 45 | + |
| 46 | ++ # Strip caller-supplied credential headers on cross-origin |
| 47 | ++ # redirect (different scheme/host/port). Same fix shape as |
| 48 | ++ # libcurl CVE-2018-1000007. Opt-out via |
| 49 | ++ # allow_credentialed_redirects => 1. |
| 50 | ++ unless ($self->{allow_credentialed_redirects}) { |
| 51 | ++ my $orig = $request->uri; |
| 52 | ++ my $new = $referral->uri; |
| 53 | ++ my $orig_scheme = defined $orig->scheme ? $orig->scheme : q{}; |
| 54 | ++ my $new_scheme = defined $new->scheme ? $new->scheme : q{}; |
| 55 | ++ my $orig_host = defined $orig->host ? lc $orig->host : q{}; |
| 56 | ++ my $new_host = defined $new->host ? lc $new->host : q{}; |
| 57 | ++ my $orig_port = eval { $orig->port } || 0; |
| 58 | ++ my $new_port = eval { $new->port } || 0; |
| 59 | ++ if ( $orig_scheme ne $new_scheme |
| 60 | ++ || $orig_host ne $new_host |
| 61 | ++ || $orig_port != $new_port) |
| 62 | ++ { |
| 63 | ++ $referral->remove_header('Authorization', 'Proxy-Authorization'); |
| 64 | ++ } |
| 65 | ++ } |
| 66 | ++ |
| 67 | ++ # Refuse https->http downgrade by default. A caller who |
| 68 | ++ # requested https reasonably expects end-to-end TLS; following |
| 69 | ++ # a 3xx to plaintext leaks the body and remaining headers. |
| 70 | ++ # Opt-out via allow_downgrade => 1. |
| 71 | ++ my $orig_scheme = defined $request->uri->scheme ? $request->uri->scheme : q{}; |
| 72 | ++ my $new_scheme = defined $referral->uri->scheme ? $referral->uri->scheme : q{}; |
| 73 | ++ if ( $orig_scheme eq 'https' |
| 74 | ++ && $new_scheme eq 'http' |
| 75 | ++ && !$self->{allow_downgrade}) |
| 76 | ++ { |
| 77 | ++ $response->header("Client-Warning" => |
| 78 | ++ "Refusing https->http redirect (set allow_downgrade => 1 to opt in)"); |
| 79 | ++ return $response; |
| 80 | ++ } |
| 81 | ++ |
| 82 | + return $response unless $self->redirect_ok($referral, $response); |
| 83 | + return $self->request($referral, $arg, $size, $response); |
| 84 | + |
| 85 | +@@ -660,6 +701,8 @@ sub is_protocol_supported |
| 86 | + sub protocols_allowed { shift->_elem('protocols_allowed' , @_) } |
| 87 | + sub protocols_forbidden { shift->_elem('protocols_forbidden' , @_) } |
| 88 | + sub requests_redirectable { shift->_elem('requests_redirectable', @_) } |
| 89 | ++sub allow_credentialed_redirects { shift->_elem('allow_credentialed_redirects', @_) } |
| 90 | ++sub allow_downgrade { shift->_elem('allow_downgrade', @_) } |
| 91 | + |
| 92 | + |
| 93 | + sub redirect_ok |
| 94 | +@@ -1269,6 +1312,8 @@ The following options correspond to attr |
| 95 | + KEY DEFAULT |
| 96 | + ----------- -------------------- |
| 97 | + agent "libwww-perl/#.###" |
| 98 | ++ allow_credentialed_redirects undef |
| 99 | ++ allow_downgrade undef |
| 100 | + conn_cache undef |
| 101 | + cookie_jar undef |
| 102 | + default_headers HTTP::Headers->new |
| 103 | +@@ -1285,6 +1330,18 @@ The following options correspond to attr |
| 104 | + ssl_opts { verify_hostname => 1 } |
| 105 | + timeout 180 |
| 106 | + |
| 107 | ++When following a 3xx redirect to a different origin (a different |
| 108 | ++scheme, host, or port), LWP::UserAgent strips C<Authorization> |
| 109 | ++and C<Proxy-Authorization> from the cloned request to avoid leaking |
| 110 | ++caller-supplied credentials to the redirect target. Set |
| 111 | ++C<allow_credentialed_redirects> to a true value to opt out and |
| 112 | ++forward these headers across origins. |
| 113 | ++ |
| 114 | ++A 3xx redirect that downgrades an C<https> request to plain C<http> |
| 115 | ++is refused by default; the original response is returned with a |
| 116 | ++C<Client-Warning> header explaining the refusal. Set C<allow_downgrade> |
| 117 | ++to a true value to opt in to following such redirects. |
| 118 | ++ |
| 119 | + The following additional options are also accepted: If the C<env_proxy> option |
| 120 | + is passed in with a true value, then proxy settings are read from environment |
| 121 | + variables (see L<LWP::UserAgent/env_proxy>). If C<env_proxy> isn't provided, the |
| 122 | +@@ -1327,6 +1384,30 @@ string is appended to it. |
| 123 | + The user agent string should be one or more simple product identifiers |
| 124 | + with an optional version number separated by the C</> character. |
| 125 | + |
| 126 | ++=head2 allow_credentialed_redirects |
| 127 | ++ |
| 128 | ++ my $allow = $ua->allow_credentialed_redirects; |
| 129 | ++ $ua->allow_credentialed_redirects( 1 ); |
| 130 | ++ |
| 131 | ++Get/set whether caller-supplied C<Authorization> and C<Proxy-Authorization> |
| 132 | ++headers are forwarded across cross-origin 3xx redirects (a different scheme, |
| 133 | ++host, or port). Defaults to a false value, meaning the headers are stripped |
| 134 | ++on cross-origin redirects to avoid leaking credentials to the redirect target. |
| 135 | ++Same-origin redirects always retain these headers. |
| 136 | ++ |
| 137 | ++=head2 allow_downgrade |
| 138 | ++ |
| 139 | ++ my $allow = $ua->allow_downgrade; |
| 140 | ++ $ua->allow_downgrade( 1 ); |
| 141 | ++ |
| 142 | ++Get/set whether a 3xx redirect from an C<https> request to a plain |
| 143 | ++C<http> URL is followed. Defaults to a false value, meaning such |
| 144 | ++redirects are refused; the original response is returned with a |
| 145 | ++C<Client-Warning> header. Set to a true value to opt in to following |
| 146 | ++the redirect. Note that even when C<allow_downgrade> is true, |
| 147 | ++cross-origin credential stripping still applies (see |
| 148 | ++L</allow_credentialed_redirects>). |
| 149 | ++ |
| 150 | + =head2 conn_cache |
| 151 | + |
| 152 | + my $cache_obj = $ua->conn_cache; |
0 commit comments