Skip to content

Commit dada989

Browse files
authored
Fix a potential exponential backtracking issue when parsing quoted headers (#1434)
1 parent 3ce9451 commit dada989

File tree

5 files changed

+97
-3
lines changed

5 files changed

+97
-3
lines changed

pkgs/http_parser/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 4.1.2-wip
2+
3+
* Fixed a bug where parsing quoted header values could require a regex to
4+
backtrack
5+
* Fixed a bug where quoted header values containing escaped quotes would not
6+
be correctly parsed.
7+
18
## 4.1.1
29

310
* Move to `dart-lang/http` monorepo.

pkgs/http_parser/lib/src/scan.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ final token = RegExp(r'[^()<>@,;:"\\/[\]?={} \t\x00-\x1F\x7F]+');
1111
final _lws = RegExp(r'(?:\r\n)?[ \t]+');
1212

1313
/// A quoted string.
14-
final _quotedString = RegExp(r'"(?:[^"\x00-\x1F\x7F]|\\.)*"');
14+
///
15+
/// [RFC-2616 2.2](https://datatracker.ietf.org/doc/html/rfc2616#section-2.2)
16+
/// defines the `quoted-string` production. This expression is identical to
17+
/// the RFC definition expect that, in this regex, `qdtext` is not allowed to
18+
/// contain `"\"`.
19+
final _quotedString = RegExp(r'"(?:[^"\x00-\x1F\x7F\\]|\\.)*"');
1520

1621
/// A quoted pair.
1722
final _quotedPair = RegExp(r'\\(.)');
@@ -54,7 +59,7 @@ List<T> parseList<T>(StringScanner scanner, T Function() parseElement) {
5459
return result;
5560
}
5661

57-
/// Parses a single quoted string, and returns its contents.
62+
/// Parses a double quoted string, and returns its contents.
5863
///
5964
/// If [name] is passed, it's used to describe the expected value if it's not
6065
/// found.

pkgs/http_parser/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: http_parser
2-
version: 4.1.1
2+
version: 4.1.2-wip
33
description: >-
44
A platform-independent package for parsing and serializing HTTP formats.
55
repository: https://github.com/dart-lang/http/tree/master/pkgs/http_parser

pkgs/http_parser/test/authentication_challenge_test.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ void _singleChallengeTests(
7373
equals({'realm': 'fblthp, foo=bar', 'baz': 'qux'}));
7474
});
7575

76+
test('parses quoted string parameters with surrounding spaces', () {
77+
final challenge =
78+
parseChallenge('scheme realm= "fblthp, foo=bar" , baz= "qux" ');
79+
expect(challenge.scheme, equals('scheme'));
80+
expect(challenge.parameters,
81+
equals({'realm': 'fblthp, foo=bar', 'baz': 'qux'}));
82+
});
83+
7684
test('normalizes the case of the scheme', () {
7785
final challenge = parseChallenge('ScHeMe realm=fblthp');
7886
expect(challenge.scheme, equals('scheme'));

pkgs/http_parser/test/scan_test.dart

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:http_parser/src/scan.dart';
6+
import 'package:string_scanner/string_scanner.dart';
7+
import 'package:test/test.dart';
8+
9+
void main() {
10+
group('expectQuotedString', () {
11+
test('no open quote', () {
12+
final scanner = StringScanner('test"');
13+
expect(
14+
() => expectQuotedString(scanner),
15+
throwsA(isA<StringScannerException>()
16+
.having((e) => e.offset, 'offset', 0)
17+
.having((e) => e.message, 'message', 'expected quoted string.')
18+
.having((e) => e.source, 'source', 'test"')));
19+
expect(scanner.isDone, isFalse);
20+
expect(scanner.lastMatch, null);
21+
expect(scanner.position, 0);
22+
});
23+
24+
test('no close quote', () {
25+
final scanner = StringScanner('"test');
26+
expect(
27+
() => expectQuotedString(scanner),
28+
throwsA(isA<StringScannerException>()
29+
.having((e) => e.offset, 'offset', 0)
30+
.having((e) => e.message, 'message', 'expected quoted string.')
31+
.having((e) => e.source, 'source', '"test')));
32+
expect(scanner.isDone, isFalse);
33+
expect(scanner.lastMatch, null);
34+
expect(scanner.position, 0);
35+
});
36+
37+
test('simple quoted', () {
38+
final scanner = StringScanner('"test"');
39+
expect(expectQuotedString(scanner), 'test');
40+
expect(scanner.isDone, isTrue);
41+
expect(scanner.lastMatch?.group(0), '"test"');
42+
expect(scanner.position, 6);
43+
});
44+
45+
test(r'escaped \', () {
46+
final scanner = StringScanner(r'"escaped: \\"');
47+
expect(expectQuotedString(scanner), r'escaped: \');
48+
expect(scanner.isDone, isTrue);
49+
expect(scanner.lastMatch?.group(0), r'"escaped: \\"');
50+
expect(scanner.position, 13);
51+
});
52+
53+
test(r'bare \', () {
54+
final scanner = StringScanner(r'"bare: \"');
55+
expect(
56+
() => expectQuotedString(scanner),
57+
throwsA(isA<StringScannerException>()
58+
.having((e) => e.offset, 'offset', 0)
59+
.having((e) => e.message, 'message', 'expected quoted string.')
60+
.having((e) => e.source, 'source', r'"bare: \"')));
61+
expect(scanner.isDone, isFalse);
62+
expect(scanner.lastMatch, null);
63+
expect(scanner.position, 0);
64+
});
65+
66+
test(r'escaped "', () {
67+
final scanner = StringScanner(r'"escaped: \""');
68+
expect(expectQuotedString(scanner), r'escaped: "');
69+
expect(scanner.isDone, isTrue);
70+
expect(scanner.lastMatch?.group(0), r'"escaped: \""');
71+
expect(scanner.position, 13);
72+
});
73+
});
74+
}

0 commit comments

Comments
 (0)