diff --git a/sdk/lib/_http/http_impl.dart b/sdk/lib/_http/http_impl.dart index 804bacc2222..e0c1e2f6a12 100644 --- a/sdk/lib/_http/http_impl.dart +++ b/sdk/lib/_http/http_impl.dart @@ -3059,11 +3059,8 @@ class _HttpClient implements HttpClient { ); } - static bool _isSubdomain(Uri subdomain, Uri domain) { - return (subdomain.isScheme(domain.scheme) && - subdomain.port == domain.port && - (subdomain.host == domain.host || - subdomain.host.endsWith("." + domain.host))); + static bool _isSameOrigin(Uri a, Uri b) { + return a.isScheme(b.scheme) && a.host == b.host && a.port == b.port; } // Only visible for testing. @@ -3072,17 +3069,16 @@ class _HttpClient implements HttpClient { required Uri originalUrl, required Uri redirectUrl, }) { - if (_isSubdomain(redirectUrl, originalUrl)) { - return true; - } - - const nonRedirectHeaders = [ + // It is only safe to copy sensitive headers when redirecting to the + // same origin (RFC 6454 section 4). + const sensitiveHeaders = [ "authorization", "www-authenticate", "cookie", "cookie2", ]; - return !nonRedirectHeaders.contains(headerKey.toLowerCase()); + return !sensitiveHeaders.contains(headerKey.toLowerCase()) || + _isSameOrigin(redirectUrl, originalUrl); } Future<_HttpClientRequest> _openUrlFromRequest( diff --git a/tests/standalone/io/http_redirect_test.dart b/tests/standalone/io/http_redirect_test.dart index 4672157bcec..50451afbb72 100644 --- a/tests/standalone/io/http_redirect_test.dart +++ b/tests/standalone/io/http_redirect_test.dart @@ -582,15 +582,70 @@ void testShouldCopyHeadersOnRedirect() { ); checkShouldCopyHeader("cat", "http://foo.com", "http://foo.com:80/foo", true); - // Redirect to subdomain. + // Redirect to subdomain. Credential-carrying headers MUST NOT be forwarded + // to a subdomain (the subdomain can be attacker-controlled on shared apex + // hosts, and RFC 6265 host-only cookies must not be sent to subdomains). + // Non-sensitive headers continue to follow the redirect. checkShouldCopyHeader( "authorization", "https://foo.com", "https://www.foo.com", - true, + false, + ); + checkShouldCopyHeader( + "cookie", + "https://foo.com", + "https://www.foo.com", + false, + ); + checkShouldCopyHeader( + "cookie2", + "https://foo.com", + "https://www.foo.com", + false, + ); + checkShouldCopyHeader( + "www-authenticate", + "https://foo.com", + "https://www.foo.com", + false, ); checkShouldCopyHeader("cat", "https://foo.com", "https://www.foo.com", true); + // Redirect to same host: all sensitive headers are still forwarded. + checkShouldCopyHeader( + "cookie", + "https://foo.com", + "https://foo.com/path", + true, + ); + checkShouldCopyHeader( + "cookie2", + "https://foo.com", + "https://foo.com/path", + true, + ); + checkShouldCopyHeader( + "www-authenticate", + "https://foo.com", + "https://foo.com/path", + true, + ); + + // Redirect to a deeper subdomain (e.g. attacker-uploads.example.com). + checkShouldCopyHeader( + "authorization", + "https://example.com", + "https://attacker-uploads.example.com", + false, + ); + checkShouldCopyHeader( + "cookie", + "https://example.com", + "https://attacker-uploads.example.com", + false, + ); + // Redirect to different domain. checkShouldCopyHeader( "authorization",