Skip to content

Commit e2bc045

Browse files
authored
Fix WhiteSpace validation in HttpHeaders (#102693)
1 parent ba6089e commit e2bc045

11 files changed

+125
-197
lines changed

src/libraries/System.Net.Http/src/Resources/Strings.resx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,6 @@
147147
<data name="net_http_headers_invalid_host_header" xml:space="preserve">
148148
<value>The specified value is not a valid 'Host' header string.</value>
149149
</data>
150-
<data name="net_http_headers_invalid_etag_name" xml:space="preserve">
151-
<value>The specified value is not a valid quoted string.</value>
152-
</data>
153150
<data name="net_http_headers_invalid_range" xml:space="preserve">
154151
<value>Invalid range. At least one of the two parameters must not be null.</value>
155152
</data>

src/libraries/System.Net.Http/src/System/Net/Http/Headers/ContentDispositionHeaderValue.cs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Diagnostics;
66
using System.Diagnostics.CodeAnalysis;
77
using System.Globalization;
8-
using System.Runtime.CompilerServices;
98
using System.Text;
109

1110
namespace System.Net.Http.Headers
@@ -35,7 +34,7 @@ public string DispositionType
3534
get { return _dispositionType; }
3635
set
3736
{
38-
CheckDispositionTypeFormat(value);
37+
HeaderUtilities.CheckValidToken(value);
3938
_dispositionType = value;
4039
}
4140
}
@@ -125,7 +124,7 @@ public long? Size
125124

126125
#region Constructors
127126

128-
internal ContentDispositionHeaderValue()
127+
private ContentDispositionHeaderValue()
129128
{
130129
// Used by the parser to create a new instance of this type.
131130
}
@@ -140,7 +139,8 @@ protected ContentDispositionHeaderValue(ContentDispositionHeaderValue source)
140139

141140
public ContentDispositionHeaderValue(string dispositionType)
142141
{
143-
CheckDispositionTypeFormat(dispositionType);
142+
HeaderUtilities.CheckValidToken(dispositionType);
143+
144144
_dispositionType = dispositionType;
145145
}
146146

@@ -271,19 +271,6 @@ private static int GetDispositionTypeExpressionLength(string input, int startInd
271271
return typeLength;
272272
}
273273

274-
private static void CheckDispositionTypeFormat(string dispositionType, [CallerArgumentExpression(nameof(dispositionType))] string? parameterName = null)
275-
{
276-
ArgumentException.ThrowIfNullOrWhiteSpace(dispositionType, parameterName);
277-
278-
// When adding values using strongly typed objects, no leading/trailing LWS (whitespace) are allowed.
279-
int dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out string? tempDispositionType);
280-
if ((dispositionTypeLength == 0) || (tempDispositionType!.Length != dispositionType.Length))
281-
{
282-
throw new FormatException(SR.Format(System.Globalization.CultureInfo.InvariantCulture,
283-
SR.net_http_headers_invalid_value, dispositionType));
284-
}
285-
}
286-
287274
#endregion Parsing
288275

289276
#region Helpers

src/libraries/System.Net.Http/src/System/Net/Http/Headers/EntityTagHeaderValue.cs

Lines changed: 33 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,25 @@ namespace System.Net.Http.Headers
88
{
99
public class EntityTagHeaderValue : ICloneable
1010
{
11-
private readonly string _tag;
12-
private readonly bool _isWeak;
11+
public string Tag { get; private init; }
1312

14-
public string Tag
15-
{
16-
get { return _tag; }
17-
}
18-
19-
public bool IsWeak
20-
{
21-
get { return _isWeak; }
22-
}
13+
public bool IsWeak { get; private init; }
2314

24-
public static EntityTagHeaderValue Any { get; } = new EntityTagHeaderValue();
15+
public static EntityTagHeaderValue Any { get; } = new EntityTagHeaderValue("*", isWeak: false, false);
2516

26-
private EntityTagHeaderValue()
17+
private EntityTagHeaderValue(string tag, bool isWeak, bool _)
2718
{
28-
_tag = "*";
19+
#if DEBUG
20+
// This constructor should only be used with already validated values.
21+
// "*" is a special case that can only be created via the static Any property.
22+
if (tag != "*")
23+
{
24+
new EntityTagHeaderValue(tag, isWeak);
25+
}
26+
#endif
27+
28+
Tag = tag;
29+
IsWeak = isWeak;
2930
}
3031

3132
public EntityTagHeaderValue(string tag)
@@ -35,56 +36,31 @@ public EntityTagHeaderValue(string tag)
3536

3637
public EntityTagHeaderValue(string tag, bool isWeak)
3738
{
38-
ArgumentException.ThrowIfNullOrWhiteSpace(tag);
39+
HeaderUtilities.CheckValidQuotedString(tag);
3940

40-
int length;
41-
if ((HttpRuleParser.GetQuotedStringLength(tag, 0, out length) != HttpParseResult.Parsed) ||
42-
(length != tag.Length))
43-
{
44-
// Note that we don't allow 'W/' prefixes for weak ETags in the 'tag' parameter. If the user wants to
45-
// add a weak ETag, they can set 'isWeak' to true.
46-
throw new FormatException(SR.net_http_headers_invalid_etag_name);
47-
}
48-
49-
_tag = tag;
50-
_isWeak = isWeak;
41+
Tag = tag;
42+
IsWeak = isWeak;
5143
}
5244

5345
private EntityTagHeaderValue(EntityTagHeaderValue source)
5446
{
5547
Debug.Assert(source != null);
5648

57-
_tag = source._tag;
58-
_isWeak = source._isWeak;
49+
Tag = source.Tag;
50+
IsWeak = source.IsWeak;
5951
}
6052

61-
public override string ToString()
62-
{
63-
if (_isWeak)
64-
{
65-
return "W/" + _tag;
66-
}
67-
return _tag;
68-
}
69-
70-
public override bool Equals([NotNullWhen(true)] object? obj)
71-
{
72-
EntityTagHeaderValue? other = obj as EntityTagHeaderValue;
73-
74-
if (other == null)
75-
{
76-
return false;
77-
}
53+
public override string ToString() =>
54+
IsWeak ? $"W/{Tag}" : Tag;
7855

56+
public override bool Equals([NotNullWhen(true)] object? obj) =>
57+
obj is EntityTagHeaderValue other &&
58+
IsWeak == other.IsWeak &&
7959
// Since the tag is a quoted-string we treat it case-sensitive.
80-
return ((_isWeak == other._isWeak) && string.Equals(_tag, other._tag, StringComparison.Ordinal));
81-
}
60+
string.Equals(Tag, other.Tag, StringComparison.Ordinal);
8261

83-
public override int GetHashCode()
84-
{
85-
// Since the tag is a quoted-string we treat it case-sensitive.
86-
return _tag.GetHashCode() ^ _isWeak.GetHashCode();
87-
}
62+
public override int GetHashCode() =>
63+
HashCode.Combine(Tag, IsWeak);
8864

8965
public static EntityTagHeaderValue Parse(string input)
9066
{
@@ -144,24 +120,15 @@ internal static int GetEntityTagLength(string? input, int startIndex, out Entity
144120
current += HttpRuleParser.GetWhitespaceLength(input, current);
145121
}
146122

147-
int tagStartIndex = current;
148-
int tagLength;
149-
if (current == input.Length || HttpRuleParser.GetQuotedStringLength(input, current, out tagLength) != HttpParseResult.Parsed)
123+
if (current == input.Length || HttpRuleParser.GetQuotedStringLength(input, current, out int tagLength) != HttpParseResult.Parsed)
150124
{
151125
return 0;
152126
}
153127

154-
if (tagLength == input.Length)
155-
{
156-
// Most of the time we'll have strong ETags without leading/trailing whitespace.
157-
Debug.Assert(startIndex == 0);
158-
Debug.Assert(!isWeak);
159-
parsedValue = new EntityTagHeaderValue(input);
160-
}
161-
else
162-
{
163-
parsedValue = new EntityTagHeaderValue(input.Substring(tagStartIndex, tagLength), isWeak);
164-
}
128+
// Most of the time we'll have strong ETags without leading/trailing whitespace.
129+
Debug.Assert(tagLength != input.Length || (startIndex == 0 && !isWeak));
130+
131+
parsedValue = new EntityTagHeaderValue(input.Substring(current, tagLength), isWeak, false);
165132

166133
current += tagLength;
167134
}

src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,19 @@ private static void AddHexEscaped(byte c, ref ValueStringBuilder destination)
142142

143143
internal static void CheckValidToken(string value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
144144
{
145-
ArgumentException.ThrowIfNullOrWhiteSpace(value, parameterName);
145+
ArgumentException.ThrowIfNullOrEmpty(value, parameterName);
146146

147-
if (HttpRuleParser.GetTokenLength(value, 0) != value.Length)
147+
if (!HttpRuleParser.IsToken(value))
148148
{
149149
throw new FormatException(SR.Format(CultureInfo.InvariantCulture, SR.net_http_headers_invalid_value, value));
150150
}
151151
}
152152

153153
internal static void CheckValidComment(string value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
154154
{
155-
ArgumentException.ThrowIfNullOrWhiteSpace(value, parameterName);
155+
ArgumentException.ThrowIfNullOrEmpty(value, parameterName);
156156

157-
int length;
158-
if ((HttpRuleParser.GetCommentLength(value, 0, out length) != HttpParseResult.Parsed) ||
157+
if ((HttpRuleParser.GetCommentLength(value, 0, out int length) != HttpParseResult.Parsed) ||
159158
(length != value.Length)) // no trailing spaces allowed
160159
{
161160
throw new FormatException(SR.Format(CultureInfo.InvariantCulture, SR.net_http_headers_invalid_value, value));
@@ -164,10 +163,9 @@ internal static void CheckValidComment(string value, [CallerArgumentExpression(n
164163

165164
internal static void CheckValidQuotedString(string value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
166165
{
167-
ArgumentException.ThrowIfNullOrWhiteSpace(value, parameterName);
166+
ArgumentException.ThrowIfNullOrEmpty(value, parameterName);
168167

169-
int length;
170-
if ((HttpRuleParser.GetQuotedStringLength(value, 0, out length) != HttpParseResult.Parsed) ||
168+
if ((HttpRuleParser.GetQuotedStringLength(value, 0, out int length) != HttpParseResult.Parsed) ||
171169
(length != value.Length)) // no trailing spaces allowed
172170
{
173171
throw new FormatException(SR.Format(CultureInfo.InvariantCulture, SR.net_http_headers_invalid_value, value));

src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaders.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,7 @@ private static void ParseAndAddValue(HeaderDescriptor descriptor, HeaderStoreIte
10261026

10271027
private HeaderDescriptor GetHeaderDescriptor(string name)
10281028
{
1029-
ArgumentException.ThrowIfNullOrWhiteSpace(name);
1029+
ArgumentException.ThrowIfNullOrEmpty(name);
10301030

10311031
if (!HeaderDescriptor.TryGet(name, out HeaderDescriptor descriptor))
10321032
{

src/libraries/System.Net.Http/src/System/Net/Http/Headers/MediaTypeHeaderValue.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using System.Diagnostics.CodeAnalysis;
77
using System.Runtime.CompilerServices;
88
using System.Text;
9-
using static System.HexConverter;
109

1110
namespace System.Net.Http.Headers
1211
{
@@ -275,7 +274,7 @@ private static int GetMediaTypeExpressionLength(string input, int startIndex, ou
275274

276275
private static void CheckMediaTypeFormat(string mediaType, [CallerArgumentExpression(nameof(mediaType))] string? parameterName = null)
277276
{
278-
ArgumentException.ThrowIfNullOrWhiteSpace(mediaType, parameterName);
277+
ArgumentException.ThrowIfNullOrEmpty(mediaType, parameterName);
279278

280279
// When adding values using strongly typed objects, no leading/trailing LWS (whitespace) are allowed.
281280
// Also no LWS between type and subtype are allowed.

src/libraries/System.Net.Http/src/System/Net/Http/Headers/ViaHeaderValue.cs

Lines changed: 31 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
6-
using System.IO;
76
using System.Text;
87

98
namespace System.Net.Http.Headers
@@ -15,24 +14,25 @@ public class ViaHeaderValue : ICloneable
1514
private readonly string _receivedBy;
1615
private readonly string? _comment;
1716

18-
public string? ProtocolName
19-
{
20-
get { return _protocolName; }
21-
}
17+
public string? ProtocolName => _protocolName;
2218

23-
public string ProtocolVersion
24-
{
25-
get { return _protocolVersion; }
26-
}
19+
public string ProtocolVersion => _protocolVersion;
2720

28-
public string ReceivedBy
29-
{
30-
get { return _receivedBy; }
31-
}
21+
public string ReceivedBy => _receivedBy;
22+
23+
public string? Comment => _comment;
3224

33-
public string? Comment
25+
private ViaHeaderValue(string protocolVersion, string receivedBy, string? protocolName, string? comment, bool _)
3426
{
35-
get { return _comment; }
27+
#if DEBUG
28+
// This constructor should only be used with already validated values.
29+
new ViaHeaderValue(protocolVersion, receivedBy, protocolName, comment);
30+
#endif
31+
32+
_protocolVersion = protocolVersion;
33+
_receivedBy = receivedBy;
34+
_protocolName = protocolName;
35+
_comment = comment;
3636
}
3737

3838
public ViaHeaderValue(string protocolVersion, string receivedBy)
@@ -99,40 +99,21 @@ public override string ToString()
9999
return sb.ToString();
100100
}
101101

102-
public override bool Equals([NotNullWhen(true)] object? obj)
103-
{
104-
ViaHeaderValue? other = obj as ViaHeaderValue;
105-
106-
if (other == null)
107-
{
108-
return false;
109-
}
110-
102+
public override bool Equals([NotNullWhen(true)] object? obj) =>
103+
obj is ViaHeaderValue other &&
111104
// Note that for token and host case-insensitive comparison is used. Comments are compared using case-
112105
// sensitive comparison.
113-
return string.Equals(_protocolVersion, other._protocolVersion, StringComparison.OrdinalIgnoreCase) &&
114-
string.Equals(_receivedBy, other._receivedBy, StringComparison.OrdinalIgnoreCase) &&
115-
string.Equals(_protocolName, other._protocolName, StringComparison.OrdinalIgnoreCase) &&
116-
string.Equals(_comment, other._comment, StringComparison.Ordinal);
117-
}
118-
119-
public override int GetHashCode()
120-
{
121-
int result = StringComparer.OrdinalIgnoreCase.GetHashCode(_protocolVersion) ^
122-
StringComparer.OrdinalIgnoreCase.GetHashCode(_receivedBy);
123-
124-
if (!string.IsNullOrEmpty(_protocolName))
125-
{
126-
result ^= StringComparer.OrdinalIgnoreCase.GetHashCode(_protocolName);
127-
}
128-
129-
if (!string.IsNullOrEmpty(_comment))
130-
{
131-
result ^= _comment.GetHashCode();
132-
}
133-
134-
return result;
135-
}
106+
string.Equals(_protocolVersion, other._protocolVersion, StringComparison.OrdinalIgnoreCase) &&
107+
string.Equals(_receivedBy, other._receivedBy, StringComparison.OrdinalIgnoreCase) &&
108+
string.Equals(_protocolName, other._protocolName, StringComparison.OrdinalIgnoreCase) &&
109+
string.Equals(_comment, other._comment, StringComparison.Ordinal);
110+
111+
public override int GetHashCode() =>
112+
HashCode.Combine(
113+
StringComparer.OrdinalIgnoreCase.GetHashCode(_protocolVersion),
114+
StringComparer.OrdinalIgnoreCase.GetHashCode(_receivedBy),
115+
_protocolName is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(_protocolName),
116+
_comment);
136117

137118
public static ViaHeaderValue Parse(string input)
138119
{
@@ -191,8 +172,7 @@ internal static int GetViaLength(string? input, int startIndex, out object? pars
191172
if ((current < input.Length) && (input[current] == '('))
192173
{
193174
// We have a <comment> in '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]'
194-
int commentLength;
195-
if (HttpRuleParser.GetCommentLength(input, current, out commentLength) != HttpParseResult.Parsed)
175+
if (HttpRuleParser.GetCommentLength(input, current, out int commentLength) != HttpParseResult.Parsed)
196176
{
197177
return 0; // We found a '(' character but it wasn't a valid comment. Abort.
198178
}
@@ -203,7 +183,7 @@ internal static int GetViaLength(string? input, int startIndex, out object? pars
203183
current += HttpRuleParser.GetWhitespaceLength(input, current);
204184
}
205185

206-
parsedValue = new ViaHeaderValue(protocolVersion, receivedBy!, protocolName, comment);
186+
parsedValue = new ViaHeaderValue(protocolVersion, receivedBy, protocolName, comment, false);
207187
return current - startIndex;
208188
}
209189

@@ -275,7 +255,7 @@ object ICloneable.Clone()
275255

276256
private static void CheckReceivedBy(string receivedBy)
277257
{
278-
ArgumentException.ThrowIfNullOrWhiteSpace(receivedBy);
258+
ArgumentException.ThrowIfNullOrEmpty(receivedBy);
279259

280260
// 'receivedBy' can either be a host or a token. Since a token is a valid host, we only verify if the value
281261
// is a valid host.;

0 commit comments

Comments
 (0)