CVE-2022-0451: Incorrect Authorization (CWE-863)
value: Dart SDK contains the HTTPClient in dart:io library whcih includes authorization headers when handling cross origin redirects. These headers may be explicitly set and contain sensitive information. By default, HttpClient handles redirection logic. If a request is sent to example.com with authorization header and it redirects to an attackers site, they might not expect attacker site to receive authorization header. We recommend updating the Dart SDK to version 2.16.0 or beyond.
CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 6.5
Published: 2022-02-18T14:15Z
Repository: https://github.com/dart-lang/sdk
Language: Other
Files changed: CHANGELOG.md, sdk/lib/_http/http.dart, sdk/lib/_http/http_impl.dart, tests/standalone/io/http_redirect_test.dart, tests/standalone_2/io/http_redirect_test.dart
Commit date: 2022-01-27 20:49:42 +0000
Commit message: Disable security-related headers on redirect.

TESTED=updated unit tests
Bug: https://github.com/dart-lang/sdk/issues/45410
Change-Id: I7c555a4818fd719d42748b6a18780e3d9b3ee147
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/229947
Reviewed-by: Alexander Aprelev <aam@google.com>
Commit-Queue: Brian Quinlan <bquinlan@google.com>

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 533a4bd00b6..0d2bee122cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,10 +15,17 @@
 #### `dart:indexed_db`
 
 - `IdbFactory.supportsDatabaseNames` has been deprecated. It will always return
   `false`.
 
+#### `dart:io`
+
+- **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.
+
 ### Tools
 
 #### Dart command line
 
 - **Breaking Change** [#46100](https://github.com/dart-lang/sdk/issues/46100):
diff --git a/sdk/lib/_http/http.dart b/sdk/lib/_http/http.dart
index b63102a81af..fdb4de01838 100644
--- a/sdk/lib/_http/http.dart
+++ b/sdk/lib/_http/http.dart
@@ -1713,12 +1713,43 @@ abstract class HttpClientRequest implements IOSink {
   /// [HttpStatus.seeOther] (303) automatic redirect will also happen
   /// for "POST" requests with the method changed to "GET" when
   /// 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
   /// when [followRedirects] is `true`. If this number is exceeded
   /// an error event will be added with a [RedirectException].
diff --git a/sdk/lib/_http/http_impl.dart b/sdk/lib/_http/http_impl.dart
index 6c9c786dbf4..622c662e832 100644
--- a/sdk/lib/_http/http_impl.dart
+++ b/sdk/lib/_http/http_impl.dart
@@ -665,11 +665,11 @@ class _HttpClientResponse extends _HttpInboundMessageListInt
               RedirectException("Redirect loop detected", redirects));
         }
       }
     }
     return _httpClient
-        ._openUrlFromRequest(method, url, _httpRequest)
+        ._openUrlFromRequest(method, url, _httpRequest, isRedirect: true)
         .then((request) {
       request._responseRedirects
         ..addAll(redirects)
         ..add(_RedirectInfo(statusCode, method!, url!));
       return request.close();
@@ -749,11 +749,12 @@ class _HttpClientResponse extends _HttpInboundMessageListInt
       _httpRequest._profileData?.requestEvent('Retrying');
       // Drain body and retry.
       return drain().then((_) {
         return _httpClient
             ._openUrlFromRequest(
-                _httpRequest.method, _httpRequest.uri, _httpRequest)
+                _httpRequest.method, _httpRequest.uri, _httpRequest,
+                isRedirect: false)
             .then((request) => request.close());
       });
     }
 
     List<String>? authChallenge() {
@@ -2713,12 +2714,35 @@ class _HttpClient implements HttpClient {
       profileData?.finishRequestWithError(error.toString());
       throw error;
     });
   }
 
+  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);
     return _openUrl(method, resolved).then((_HttpClientRequest request) {
       request
@@ -2726,11 +2750,13 @@ class _HttpClient implements HttpClient {
         ..followRedirects = previous.followRedirects
         // Allow same number of redirects.
         ..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]!);
         }
       }
       return request
         ..headers.chunkedTransferEncoding = false
diff --git a/tests/standalone/io/http_redirect_test.dart b/tests/standalone/io/http_redirect_test.dart
index f68e32f4b5d..1a192226c05 100644
--- a/tests/standalone/io/http_redirect_test.dart
+++ b/tests/standalone/io/http_redirect_test.dart
@@ -5,11 +5,11 @@
 
 import "package:expect/expect.dart";
 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>();
     addRequestHandler(
         String path, void handler(HttpRequest request, HttpResponse response)) {
@@ -126,20 +126,37 @@ Future<HttpServer> setupServer() {
     });
 
     // 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;
       response.close();
     });
     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);
       request.listen((_) {}, onDone: () {
         response.headers.set(HttpHeaders.locationHeader,
@@ -181,10 +198,40 @@ Future<HttpServer> setupServer() {
     completer.complete(server);
   });
   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);
   } else {
     Expect.equals(redirectCount - 1, response.redirects.length);
@@ -248,10 +295,11 @@ void testManualRedirectWithHeaders() {
     client
         .getUrl(Uri.parse("http://127.0.0.1:${server.port}/src"))
         .then((HttpClientRequest request) {
       request.followRedirects = false;
       request.headers.add("X-Request-Header", "value");
+      request.headers.add("Authorization", "Basic ...");
       return request.close();
     }).then(handleResponse);
   });
 }
 
@@ -280,10 +328,11 @@ void testAutoRedirectWithHeaders() {
 
     client
         .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"),
           onDone: () {
         Expect.equals(1, response.redirects.length);
@@ -292,10 +341,37 @@ void testAutoRedirectWithHeaders() {
       });
     });
   });
 }
 
+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();
 
     client
@@ -439,10 +515,11 @@ void testRedirectRelativeToAbsolute() {
 main() {
   testManualRedirect();
   testManualRedirectWithHeaders();
   testAutoRedirect();
   testAutoRedirectWithHeaders();
+  testCrossDomainAutoRedirectWithHeaders();
   testAutoRedirect301POST();
   testAutoRedirect303POST();
   testAutoRedirectLimit();
   testRedirectLoop();
   testRedirectClosingConnection();
diff --git a/tests/standalone_2/io/http_redirect_test.dart b/tests/standalone_2/io/http_redirect_test.dart
index e168b11b403..e851045622b 100644
--- a/tests/standalone_2/io/http_redirect_test.dart
+++ b/tests/standalone_2/io/http_redirect_test.dart
@@ -7,11 +7,11 @@
 
 import "package:expect/expect.dart";
 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>();
     addRequestHandler(
         String path, void handler(HttpRequest request, HttpResponse response)) {
@@ -128,20 +128,37 @@ Future<HttpServer> setupServer() {
     });
 
     // 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;
       response.close();
     });
     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);
       request.listen((_) {}, onDone: () {
         response.headers.set(HttpHeaders.locationHeader,
@@ -183,10 +200,40 @@ Future<HttpServer> setupServer() {
     completer.complete(server);
   });
   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);
   } else {
     Expect.equals(redirectCount - 1, response.redirects.length);
@@ -250,10 +297,11 @@ void testManualRedirectWithHeaders() {
     client
         .getUrl(Uri.parse("http://127.0.0.1:${server.port}/src"))
         .then((HttpClientRequest request) {
       request.followRedirects = false;
       request.headers.add("X-Request-Header", "value");
+      request.headers.add("Authorization", "Basic ...");
       return request.close();
     }).then(handleResponse);
   });
 }
 
@@ -282,10 +330,11 @@ void testAutoRedirectWithHeaders() {
 
     client
         .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"),
           onDone: () {
         Expect.equals(1, response.redirects.length);
@@ -294,10 +343,37 @@ void testAutoRedirectWithHeaders() {
       });
     });
   });
 }
 
+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();
 
     client
@@ -441,10 +517,11 @@ void testRedirectRelativeToAbsolute() {
 main() {
   testManualRedirect();
   testManualRedirectWithHeaders();
   testAutoRedirect();
   testAutoRedirectWithHeaders();
+  testCrossDomainAutoRedirectWithHeaders();
   testAutoRedirect301POST();
   testAutoRedirect303POST();
   testAutoRedirectLimit();
   testRedirectLoop();
   testRedirectClosingConnection();

Vulnerable code:   /// request(s). However, any body send with the request will not be
  /// part of the redirection request(s).
        ._openUrlFromRequest(method, url, _httpRequest)
                _httpRequest.method, _httpRequest.uri, _httpRequest)
      String method, Uri uri, _HttpClientRequest previous) {
        if (request.headers[header] == null) {
Future<HttpServer> setupServer() {
Future<HttpServer> setupServer() {
Fixed code: #### `dart:io`

- **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.

  /// 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.
  /// ```
        ._openUrlFromRequest(method, url, _httpRequest, isRedirect: true)
                _httpRequest.method, _httpRequest.uri, _httpRequest,
                isRedirect: false)
  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());
  }

      String method, Uri uri, _HttpClientRequest previous,
      {required bool isRedirect}) {
        if (request.headers[header] == null &&
            (!isRedirect ||
                _shouldCopyHeaderOnRedirect(header, resolved, previous.uri))) {
Future<HttpServer> setupServer({Uri? targetServer}) {
      Expect.isNotNull(request.headers.value("Authorization"),
          "expected 'Authorization' header to be set");
      Expect.isNotNull(request.headers.value("Authorization"),
          "expected 'Authorization' header to be set");
    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();
      });
    }

// 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;
}

      request.headers.add("Authorization", "Basic ...");
      request.headers.add("Authorization", "Basic ...");
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();
        });
      });
    });
  });
}

  testCrossDomainAutoRedirectWithHeaders();
Future<HttpServer> setupServer({Uri targetServer}) {
      Expect.isNotNull(request.headers.value("Authorization"),
          "expected 'Authorization' header to be set");
      Expect.isNotNull(request.headers.value("Authorization"),
          "expected 'Authorization' header to be set");
    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();
      });
    }

// 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;
}

      request.headers.add("Authorization", "Basic ...");
      request.headers.add("Authorization", "Basic ...");
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();
        });
      });
    });
  });
}

  testCrossDomainAutoRedirectWithHeaders();