CVE-2022-0451 (Other, published 2022-02-18T14:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-863 \u2014 Incorrect Authorization
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.
Repository: https://github.com/dart-lang/sdk
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 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>


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();