Add tests to verify NUL, CR & LF header value behavior (#1440)
diff --git a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart
index 7c936da..dc76d21 100644
--- a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart
+++ b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart
@@ -20,6 +20,7 @@
CupertinoClient.defaultSessionConfiguration,
canReceiveSetCookieHeaders: true,
canSendCookieHeaders: true,
+ correctlyHandlesNullHeaderValues: false,
);
} finally {
HttpClientRequestProfile.profilingEnabled = profile;
@@ -33,6 +34,7 @@
CupertinoClient.defaultSessionConfiguration,
canReceiveSetCookieHeaders: true,
canSendCookieHeaders: true,
+ correctlyHandlesNullHeaderValues: false,
);
} finally {
HttpClientRequestProfile.profilingEnabled = profile;
@@ -46,6 +48,7 @@
canWorkInIsolates: false,
canReceiveSetCookieHeaders: true,
canSendCookieHeaders: true,
+ correctlyHandlesNullHeaderValues: false,
);
});
}
diff --git a/pkgs/http/test/io/client_conformance_test.dart b/pkgs/http/test/io/client_conformance_test.dart
index 65368e5..cc4b788 100644
--- a/pkgs/http/test/io/client_conformance_test.dart
+++ b/pkgs/http/test/io/client_conformance_test.dart
@@ -14,5 +14,7 @@
IOClient.new, preservesMethodCase: false, // https://dartbug.com/54187
canReceiveSetCookieHeaders: true,
canSendCookieHeaders: true,
+ correctlyHandlesNullHeaderValues:
+ false, // https://github.com/dart-lang/sdk/issues/56636
);
}
diff --git a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
index 1a43a6b..0686c9d 100644
--- a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
+++ b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
@@ -69,6 +69,9 @@
/// If [supportsFoldedHeaders] is `false` then the tests that assume that the
/// [Client] can parse folded headers will be skipped.
///
+/// If [correctlyHandlesNullHeaderValues] is `false` then the tests that assume
+/// that the [Client] correctly deals with NUL in header values are skipped.
+///
/// If [supportsMultipartRequest] is `false` then tests that assume that
/// multipart requests can be sent will be skipped.
///
@@ -83,6 +86,7 @@
bool canWorkInIsolates = true,
bool preservesMethodCase = false,
bool supportsFoldedHeaders = true,
+ bool correctlyHandlesNullHeaderValues = true,
bool canSendCookieHeaders = false,
bool canReceiveSetCookieHeaders = false,
bool supportsMultipartRequest = true,
@@ -97,7 +101,8 @@
testRequestHeaders(clientFactory());
testRequestMethods(clientFactory(), preservesMethodCase: preservesMethodCase);
testResponseHeaders(clientFactory(),
- supportsFoldedHeaders: supportsFoldedHeaders);
+ supportsFoldedHeaders: supportsFoldedHeaders,
+ correctlyHandlesNullHeaderValues: correctlyHandlesNullHeaderValues);
testResponseStatusLine(clientFactory());
testRedirect(clientFactory(), redirectAlwaysAllowed: redirectAlwaysAllowed);
testServerErrors(clientFactory());
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart b/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart
index 4ecb144..7d02353 100644
--- a/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart
+++ b/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart
@@ -11,8 +11,15 @@
if (dart.library.js_interop) 'response_headers_server_web.dart';
/// Tests that the [Client] correctly processes response headers.
+///
+/// If [supportsFoldedHeaders] is `false` then the tests that assume that the
+/// [Client] can parse folded headers will be skipped.
+///
+/// If [correctlyHandlesNullHeaderValues] is `false` then the tests that assume
+/// that the [Client] correctly deals with NUL in header values are skipped.
void testResponseHeaders(Client client,
- {bool supportsFoldedHeaders = true}) async {
+ {bool supportsFoldedHeaders = true,
+ bool correctlyHandlesNullHeaderValues = true}) async {
group('server headers', () {
late String host;
late StreamChannel<Object?> httpServerChannel;
@@ -123,6 +130,77 @@
matches(r'apple[ \t]*,[ \t]*orange[ \t]*,[ \t]*banana'));
});
+ group('invalid headers values', () {
+ // From RFC-9110:
+ // Field values containing CR, LF, or NUL characters are invalid and
+ // dangerous, due to the varying ways that implementations might parse and
+ // interpret those characters; a recipient of CR, LF, or NUL within a
+ // field value MUST either reject the message or replace each of those
+ // characters with SP before further processing or forwarding of that
+ // message.
+ test('NUL', () async {
+ httpServerChannel.sink.add('invalid: 1\x002\r\n');
+
+ try {
+ final response = await client.get(Uri.http(host, ''));
+ expect(response.headers['invalid'], '1 2');
+ } on ClientException {
+ // The client rejected the response, which is allowed per RFC-9110.
+ }
+ },
+ skip: !correctlyHandlesNullHeaderValues
+ ? 'does not correctly handle NUL in header values'
+ : false);
+
+ // Bare CR/LF seem to be interpreted the same as CR + LF by most clients
+ // so allow that behavior.
+ test('LF', () async {
+ httpServerChannel.sink.add('foo: 1\n2\r\n');
+
+ try {
+ final response = await client.get(Uri.http(host, ''));
+ expect(
+ response.headers['foo'],
+ anyOf(
+ '1 2', // RFC-specified behavior
+ '1' // Common client behavior.
+ ));
+ } on ClientException {
+ // The client rejected the response, which is allowed per RFC-9110.
+ }
+ });
+
+ test('CR', () async {
+ httpServerChannel.sink.add('foo: 1\r2\r\n');
+
+ try {
+ final response = await client.get(Uri.http(host, ''));
+ expect(
+ response.headers['foo'],
+ anyOf(
+ '1 2', // RFC-specified behavior
+ '1' // Common client behavior.
+ ));
+ } on ClientException {
+ // The client rejected the response, which is allowed per RFC-9110.
+ }
+ });
+ });
+
+ test('quotes', () async {
+ httpServerChannel.sink.add('FOO: "1, 2, 3"\r\n');
+
+ final response = await client.get(Uri.http(host, ''));
+ expect(response.headers['foo'], '"1, 2, 3"');
+ });
+
+ test('nested quotes', () async {
+ httpServerChannel.sink.add('FOO: "\\"1, 2, 3\\""\r\n');
+
+ final response = await client.get(Uri.http(host, ''));
+ expect(response.headers['foo'], '"\\"1, 2, 3\\""');
+ });
+
group('content length', () {
test('surrounded in spaces', () async {
// RFC-2616 4.2 says: