Version 2.16.0-134.6.beta
* Cherry-pick 57db739be0ad4629079bfa94840064f615d35abc to beta
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e43ad5..b4c038a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,11 @@
in 2018, as it doesn't work with any Dart 2.x release.
- Add optional `sourcePort` parameter to `Socket.connect`, `Socket.startConnect`, `RawSocket.connect` and `RawSocket.startConnect`
+- **Breaking Change** [#45410](https://github.com/dart-lang/sdk/issues/45410):
+ `HttpClient` no longer transmits some headers (i.e. `authorization`,
+ `www-authenticate`, `cookie`, `cookie2`) when processing redirects to
+ a different domain.
+
#### `dart:isolate`
- **Breaking Change** [#47769](https://github.com/dart-lang/sdk/issues/47769):
diff --git a/sdk/lib/_http/http.dart b/sdk/lib/_http/http.dart
index b63102a..fdb4de0 100644
--- a/sdk/lib/_http/http.dart
+++ b/sdk/lib/_http/http.dart
@@ -1715,8 +1715,39 @@
/// following the redirect.
///
/// All headers added to the request will be added to the redirection
- /// request(s). However, any body send with the request will not be
- /// part of the redirection request(s).
+ /// request(s) except when forwarding sensitive headers like
+ /// "Authorization", "WWW-Authenticate", and "Cookie". Those headers
+ /// will be skipped if following a redirect to a domain that is not a
+ /// subdomain match or exact match of the initial domain.
+ /// For example, a redirect from "foo.com" to either "foo.com" or
+ /// "sub.foo.com" will forward the sensitive headers, but a redirect to
+ /// "bar.com" will not.
+ ///
+ /// Any body send with the request will not be part of the redirection
+ /// request(s).
+ ///
+ /// For precise control of redirect handling, set this property to `false`
+ /// and make a separate HTTP request to process the redirect. For example:
+ ///
+ /// ```dart
+ /// final client = HttpClient();
+ /// var uri = Uri.parse("http://localhost/");
+ /// var request = await client.getUrl(uri);
+ /// request.followRedirects = false;
+ /// var response = await request.close();
+ /// while (response.isRedirect) {
+ /// response.drain();
+ /// final location = response.headers.value(HttpHeaders.locationHeader);
+ /// if (location != null) {
+ /// uri = uri.resolve(location);
+ /// request = await client.getUrl(uri);
+ /// // Set the body or headers as desired.
+ /// request.followRedirects = false;
+ /// response = await request.close();
+ /// }
+ /// }
+ /// // Do something with the final response.
+ /// ```
bool followRedirects = true;
/// Set this property to the maximum number of redirects to follow
diff --git a/sdk/lib/_http/http_impl.dart b/sdk/lib/_http/http_impl.dart
index 6c9c786..622c662 100644
--- a/sdk/lib/_http/http_impl.dart
+++ b/sdk/lib/_http/http_impl.dart
@@ -667,7 +667,7 @@
}
}
return _httpClient
- ._openUrlFromRequest(method, url, _httpRequest)
+ ._openUrlFromRequest(method, url, _httpRequest, isRedirect: true)
.then((request) {
request._responseRedirects
..addAll(redirects)
@@ -751,7 +751,8 @@
return drain().then((_) {
return _httpClient
._openUrlFromRequest(
- _httpRequest.method, _httpRequest.uri, _httpRequest)
+ _httpRequest.method, _httpRequest.uri, _httpRequest,
+ isRedirect: false)
.then((request) => request.close());
});
}
@@ -2715,8 +2716,31 @@
});
}
+ static bool _isSubdomain(Uri subdomain, Uri domain) {
+ return (subdomain.scheme == domain.scheme &&
+ subdomain.port == domain.port &&
+ (subdomain.host == domain.host ||
+ subdomain.host.endsWith("." + domain.host)));
+ }
+
+ static bool _shouldCopyHeaderOnRedirect(
+ String headerKey, Uri originalUrl, Uri redirectUri) {
+ if (_isSubdomain(redirectUri, originalUrl)) {
+ return true;
+ }
+
+ const nonRedirectHeaders = [
+ "authorization",
+ "www-authenticate",
+ "cookie",
+ "cookie2"
+ ];
+ return !nonRedirectHeaders.contains(headerKey.toLowerCase());
+ }
+
Future<_HttpClientRequest> _openUrlFromRequest(
- String method, Uri uri, _HttpClientRequest previous) {
+ String method, Uri uri, _HttpClientRequest previous,
+ {required bool isRedirect}) {
// If the new URI is relative (to either '/' or some sub-path),
// construct a full URI from the previous one.
Uri resolved = previous.uri.resolveUri(uri);
@@ -2728,7 +2752,9 @@
..maxRedirects = previous.maxRedirects;
// Copy headers.
for (var header in previous.headers._headers.keys) {
- if (request.headers[header] == null) {
+ if (request.headers[header] == null &&
+ (!isRedirect ||
+ _shouldCopyHeaderOnRedirect(header, resolved, previous.uri))) {
request.headers.set(header, previous.headers[header]!);
}
}
diff --git a/tests/standalone/io/http_redirect_test.dart b/tests/standalone/io/http_redirect_test.dart
index f68e32f..1a19222 100644
--- a/tests/standalone/io/http_redirect_test.dart
+++ b/tests/standalone/io/http_redirect_test.dart
@@ -7,7 +7,7 @@
import "dart:async";
import "dart:io";
-Future<HttpServer> setupServer() {
+Future<HttpServer> setupServer({Uri? targetServer}) {
final completer = new Completer<HttpServer>();
HttpServer.bind("127.0.0.1", 0).then((server) {
var handlers = new Map<String, Function>();
@@ -128,6 +128,8 @@
// Setup redirect checking headers.
addRequestHandler("/src", (HttpRequest request, HttpResponse response) {
Expect.equals("value", request.headers.value("X-Request-Header"));
+ Expect.isNotNull(request.headers.value("Authorization"),
+ "expected 'Authorization' header to be set");
response.headers.set(
HttpHeaders.locationHeader, "http://127.0.0.1:${server.port}/target");
response.statusCode = HttpStatus.movedPermanently;
@@ -135,9 +137,24 @@
});
addRequestHandler("/target", (HttpRequest request, HttpResponse response) {
Expect.equals("value", request.headers.value("X-Request-Header"));
+ Expect.isNotNull(request.headers.value("Authorization"),
+ "expected 'Authorization' header to be set");
response.close();
});
+ if (targetServer != null) {
+ addRequestHandler("/src-crossdomain",
+ (HttpRequest request, HttpResponse response) {
+ Expect.equals("value", request.headers.value("X-Request-Header"));
+ Expect.isNotNull(request.headers.value("Authorization"),
+ "expected 'Authorization' header to be set");
+ response.headers
+ .set(HttpHeaders.locationHeader, targetServer.toString());
+ response.statusCode = HttpStatus.movedPermanently;
+ response.close();
+ });
+ }
+
// Setup redirect for 301 where POST should not redirect.
addRequestHandler("/301src", (HttpRequest request, HttpResponse response) {
Expect.equals("POST", request.method);
@@ -183,6 +200,36 @@
return completer.future;
}
+// A second HTTP server used to validate that redirect requests accross domains
+// do *not* include security-related headers.
+Future<HttpServer> setupTargetServer() {
+ final completer = new Completer<HttpServer>();
+ HttpServer.bind("127.0.0.1", 0).then((server) {
+ var handlers = new Map<String, Function>();
+ addRequestHandler(
+ String path, void handler(HttpRequest request, HttpResponse response)) {
+ handlers[path] = handler;
+ }
+
+ server.listen((HttpRequest request) {
+ if (request.uri.path == "/target") {
+ Expect.equals("value", request.headers.value("X-Request-Header"));
+ Expect.isNull(request.headers.value("Authorization"),
+ "expected 'Authorization' header to be removed on redirect");
+ request.response.close();
+ } else {
+ request.listen((_) {}, onDone: () {
+ request.response.statusCode = 404;
+ request.response.close();
+ });
+ }
+ });
+
+ completer.complete(server);
+ });
+ return completer.future;
+}
+
void checkRedirects(int redirectCount, HttpClientResponse response) {
if (redirectCount < 2) {
Expect.isTrue(response.redirects.isEmpty);
@@ -250,6 +297,7 @@
.then((HttpClientRequest request) {
request.followRedirects = false;
request.headers.add("X-Request-Header", "value");
+ request.headers.add("Authorization", "Basic ...");
return request.close();
}).then(handleResponse);
});
@@ -282,6 +330,7 @@
.getUrl(Uri.parse("http://127.0.0.1:${server.port}/src"))
.then((HttpClientRequest request) {
request.headers.add("X-Request-Header", "value");
+ request.headers.add("Authorization", "Basic ...");
return request.close();
}).then((HttpClientResponse response) {
response.listen((_) => Expect.fail("Response data not expected"),
@@ -294,6 +343,33 @@
});
}
+void testCrossDomainAutoRedirectWithHeaders() {
+ setupTargetServer().then((targetServer) {
+ setupServer(
+ targetServer:
+ Uri.parse("http://127.0.0.1:${targetServer.port}/target"))
+ .then((server) {
+ HttpClient client = new HttpClient();
+
+ client
+ .getUrl(Uri.parse("http://127.0.0.1:${server.port}/src-crossdomain"))
+ .then((HttpClientRequest request) {
+ request.headers.add("X-Request-Header", "value");
+ request.headers.add("Authorization", "Basic ...");
+ return request.close();
+ }).then((HttpClientResponse response) {
+ response.listen((_) => Expect.fail("Response data not expected"),
+ onDone: () {
+ Expect.equals(1, response.redirects.length);
+ targetServer.close();
+ server.close();
+ client.close();
+ });
+ });
+ });
+ });
+}
+
void testAutoRedirect301POST() {
setupServer().then((server) {
HttpClient client = new HttpClient();
@@ -441,6 +517,7 @@
testManualRedirectWithHeaders();
testAutoRedirect();
testAutoRedirectWithHeaders();
+ testCrossDomainAutoRedirectWithHeaders();
testAutoRedirect301POST();
testAutoRedirect303POST();
testAutoRedirectLimit();
diff --git a/tests/standalone_2/io/http_redirect_test.dart b/tests/standalone_2/io/http_redirect_test.dart
index e168b11..e851045 100644
--- a/tests/standalone_2/io/http_redirect_test.dart
+++ b/tests/standalone_2/io/http_redirect_test.dart
@@ -9,7 +9,7 @@
import "dart:async";
import "dart:io";
-Future<HttpServer> setupServer() {
+Future<HttpServer> setupServer({Uri targetServer}) {
Completer completer = new Completer<HttpServer>();
HttpServer.bind("127.0.0.1", 0).then((server) {
var handlers = new Map<String, Function>();
@@ -130,6 +130,8 @@
// Setup redirect checking headers.
addRequestHandler("/src", (HttpRequest request, HttpResponse response) {
Expect.equals("value", request.headers.value("X-Request-Header"));
+ Expect.isNotNull(request.headers.value("Authorization"),
+ "expected 'Authorization' header to be set");
response.headers.set(
HttpHeaders.locationHeader, "http://127.0.0.1:${server.port}/target");
response.statusCode = HttpStatus.movedPermanently;
@@ -137,9 +139,24 @@
});
addRequestHandler("/target", (HttpRequest request, HttpResponse response) {
Expect.equals("value", request.headers.value("X-Request-Header"));
+ Expect.isNotNull(request.headers.value("Authorization"),
+ "expected 'Authorization' header to be set");
response.close();
});
+ if (targetServer != null) {
+ addRequestHandler("/src-crossdomain",
+ (HttpRequest request, HttpResponse response) {
+ Expect.equals("value", request.headers.value("X-Request-Header"));
+ Expect.isNotNull(request.headers.value("Authorization"),
+ "expected 'Authorization' header to be set");
+ response.headers
+ .set(HttpHeaders.locationHeader, targetServer.toString());
+ response.statusCode = HttpStatus.movedPermanently;
+ response.close();
+ });
+ }
+
// Setup redirect for 301 where POST should not redirect.
addRequestHandler("/301src", (HttpRequest request, HttpResponse response) {
Expect.equals("POST", request.method);
@@ -185,6 +202,36 @@
return completer.future;
}
+// A second HTTP server used to validate that redirect requests accross domains
+// do *not* include security-related headers.
+Future<HttpServer> setupTargetServer() {
+ Completer completer = new Completer<HttpServer>();
+ HttpServer.bind("127.0.0.1", 0).then((server) {
+ var handlers = new Map<String, Function>();
+ addRequestHandler(
+ String path, void handler(HttpRequest request, HttpResponse response)) {
+ handlers[path] = handler;
+ }
+
+ server.listen((HttpRequest request) {
+ if (request.uri.path == "/target") {
+ Expect.equals("value", request.headers.value("X-Request-Header"));
+ Expect.isNull(request.headers.value("Authorization"),
+ "expected 'Authorization' header to be removed on redirect");
+ request.response.close();
+ } else {
+ request.listen((_) {}, onDone: () {
+ request.response.statusCode = 404;
+ request.response.close();
+ });
+ }
+ });
+
+ completer.complete(server);
+ });
+ return completer.future;
+}
+
void checkRedirects(int redirectCount, HttpClientResponse response) {
if (redirectCount < 2) {
Expect.isTrue(response.redirects.isEmpty);
@@ -252,6 +299,7 @@
.then((HttpClientRequest request) {
request.followRedirects = false;
request.headers.add("X-Request-Header", "value");
+ request.headers.add("Authorization", "Basic ...");
return request.close();
}).then(handleResponse);
});
@@ -284,6 +332,7 @@
.getUrl(Uri.parse("http://127.0.0.1:${server.port}/src"))
.then((HttpClientRequest request) {
request.headers.add("X-Request-Header", "value");
+ request.headers.add("Authorization", "Basic ...");
return request.close();
}).then((HttpClientResponse response) {
response.listen((_) => Expect.fail("Response data not expected"),
@@ -296,6 +345,33 @@
});
}
+void testCrossDomainAutoRedirectWithHeaders() {
+ setupTargetServer().then((targetServer) {
+ setupServer(
+ targetServer:
+ Uri.parse("http://127.0.0.1:${targetServer.port}/target"))
+ .then((server) {
+ HttpClient client = new HttpClient();
+
+ client
+ .getUrl(Uri.parse("http://127.0.0.1:${server.port}/src-crossdomain"))
+ .then((HttpClientRequest request) {
+ request.headers.add("X-Request-Header", "value");
+ request.headers.add("Authorization", "Basic ...");
+ return request.close();
+ }).then((HttpClientResponse response) {
+ response.listen((_) => Expect.fail("Response data not expected"),
+ onDone: () {
+ Expect.equals(1, response.redirects.length);
+ targetServer.close();
+ server.close();
+ client.close();
+ });
+ });
+ });
+ });
+}
+
void testAutoRedirect301POST() {
setupServer().then((server) {
HttpClient client = new HttpClient();
@@ -443,6 +519,7 @@
testManualRedirectWithHeaders();
testAutoRedirect();
testAutoRedirectWithHeaders();
+ testCrossDomainAutoRedirectWithHeaders();
testAutoRedirect301POST();
testAutoRedirect303POST();
testAutoRedirectLimit();
diff --git a/tools/VERSION b/tools/VERSION
index 7e2479b..73354a3 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -28,4 +28,4 @@
MINOR 16
PATCH 0
PRERELEASE 134
-PRERELEASE_PATCH 5
\ No newline at end of file
+PRERELEASE_PATCH 6
\ No newline at end of file