1 //------------------------------------------------------------------------------
2 // <copyright file="HttpValueCollection.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 //------------------------------------------------------------------------------
6 
7 /*
8  * Ordered String/String[] collection of name/value pairs
9  * Based on NameValueCollection -- adds parsing from string, cookie collection
10  *
11  * Copyright (c) 2000 Microsoft Corporation
12  */
13 
14 namespace System.Web {
15     using System;
16     using System.Collections;
17     using System.Collections.Generic;
18     using System.Collections.Specialized;
19     using System.Linq;
20     using System.Runtime.Serialization;
21     using System.Text;
22     using System.Web.UI;
23     using System.Web.Util;
24 
25     [Serializable()]
26     internal class HttpValueCollection : NameValueCollection {
27 
28         // for implementing granular request validation
29         [NonSerialized]
30         private ValidateStringCallback _validationCallback;
31         [NonSerialized]
32         private HashSet<string> _keysAwaitingValidation;
33 
HttpValueCollection()34         internal HttpValueCollection(): base(StringComparer.OrdinalIgnoreCase) {
35         }
36 
37         // This copy constructor is used by the granular request validation feature. Since these collections are immutable
38         // once created, it's ok for us to have two collections containing the same data.
HttpValueCollection(HttpValueCollection col)39         internal HttpValueCollection(HttpValueCollection col)
40             : base(StringComparer.OrdinalIgnoreCase) {
41 
42             // We explicitly don't copy validation-related fields, as we want the copy to "reset" validation state. But we
43             // do need to go against the underlying NameObjectCollectionBase directly while copying so as to avoid triggering
44             // validation.
45             for (int i = 0; i < col.Count; i++) {
46                 ThrowIfMaxHttpCollectionKeysExceeded();
47                 string key = col.BaseGetKey(i);
48                 object value = col.BaseGet(i);
49                 BaseAdd(key, value);
50             }
51 
52             IsReadOnly = col.IsReadOnly;
53         }
54 
HttpValueCollection(String str, bool readOnly, bool urlencoded, Encoding encoding)55         internal HttpValueCollection(String str, bool readOnly, bool urlencoded, Encoding encoding): base(StringComparer.OrdinalIgnoreCase) {
56             if (!String.IsNullOrEmpty(str))
57                 FillFromString(str, urlencoded, encoding);
58 
59             IsReadOnly = readOnly;
60         }
61 
HttpValueCollection(int capacity)62         internal HttpValueCollection(int capacity) : base(capacity, StringComparer.OrdinalIgnoreCase) {
63         }
64 
HttpValueCollection(SerializationInfo info, StreamingContext context)65         protected HttpValueCollection(SerializationInfo info, StreamingContext context) : base(info, context) {
66         }
67 
68         /*
69          * We added granular request validation in ASP.NET 4.5 to provide a better request validation story for our developers.
70          * Instead of validating the entire collection ahead of time, we'll only validate entries that are actually looked at.
71          */
72 
EnableGranularValidation(ValidateStringCallback validationCallback)73         internal void EnableGranularValidation(ValidateStringCallback validationCallback) {
74             // Iterate over all the keys, adding each to the set containing the keys awaiting validation.
75             // Unlike dictionaries, HashSet<T> can contain null keys, so don't need to special-case them.
76             _keysAwaitingValidation = new HashSet<string>(Keys.Cast<string>().Where(KeyIsCandidateForValidation), StringComparer.OrdinalIgnoreCase);
77             _validationCallback = validationCallback;
78 
79             // This forces CopyTo and other methods that cache entries to flush their caches, ensuring
80             // that all values go through validation again.
81             InvalidateCachedArrays();
82         }
83 
KeyIsCandidateForValidation(string key)84         internal static bool KeyIsCandidateForValidation(string key) {
85             // Skip all our internal fields, since they don't need to be checked (VSWhidbey 275811)
86             //
87 
88             if (key != null && key.StartsWith(System.Web.UI.Page.systemPostFieldPrefix, StringComparison.Ordinal)) {
89                 return false;
90             }
91 
92             return true;
93         }
94 
EnsureKeyValidated(string key)95         private void EnsureKeyValidated(string key) {
96             if (_keysAwaitingValidation == null) {
97                 // If dynamic validation hasn't been enabled, no-op.
98                 return;
99             }
100 
101             if (!_keysAwaitingValidation.Contains(key)) {
102                 // If this key has already been validated (or is excluded), no-op.
103                 return;
104             }
105 
106             // If validation fails, the callback will throw an exception. If validation succeeds,
107             // we can remove it from the candidates list. Two notes:
108             // - Use base.Get instead of this.Get so as not to enter infinite recursion.
109             // - Eager validation skips null/empty values, so we should, also.
110             string value = base.Get(key);
111             if (!String.IsNullOrEmpty(value)) {
112                 _validationCallback(key, value);
113             }
114             _keysAwaitingValidation.Remove(key);
115         }
116 
Get(int index)117         public override string Get(int index) {
118             // Need the key so that we can pass it through validation.
119             string key = GetKey(index);
120             EnsureKeyValidated(key);
121 
122             return base.Get(index);
123         }
124 
Get(string name)125         public override string Get(string name) {
126             EnsureKeyValidated(name);
127             return base.Get(name);
128         }
129 
GetValues(int index)130         public override string[] GetValues(int index) {
131             // Need the key so that we can pass it through validation.
132             string key = GetKey(index);
133             EnsureKeyValidated(key);
134 
135             return base.GetValues(index);
136         }
137 
GetValues(string name)138         public override string[] GetValues(string name) {
139             EnsureKeyValidated(name);
140             return base.GetValues(name);
141         }
142 
143         /*
144          * END REQUEST VALIDATION
145          */
146 
MakeReadOnly()147         internal void MakeReadOnly() {
148             IsReadOnly = true;
149         }
150 
MakeReadWrite()151         internal void MakeReadWrite() {
152             IsReadOnly = false;
153         }
154 
FillFromString(String s)155         internal void FillFromString(String s) {
156             FillFromString(s, false, null);
157         }
158 
FillFromString(String s, bool urlencoded, Encoding encoding)159         internal void FillFromString(String s, bool urlencoded, Encoding encoding) {
160             int l = (s != null) ? s.Length : 0;
161             int i = 0;
162 
163             while (i < l) {
164                 // find next & while noting first = on the way (and if there are more)
165 
166                 ThrowIfMaxHttpCollectionKeysExceeded();
167 
168                 int si = i;
169                 int ti = -1;
170 
171                 while (i < l) {
172                     char ch = s[i];
173 
174                     if (ch == '=') {
175                         if (ti < 0)
176                             ti = i;
177                     }
178                     else if (ch == '&') {
179                         break;
180                     }
181 
182                     i++;
183                 }
184 
185                 // extract the name / value pair
186 
187                 String name = null;
188                 String value = null;
189 
190                 if (ti >= 0) {
191                     name = s.Substring(si, ti-si);
192                     value = s.Substring(ti+1, i-ti-1);
193                 }
194                 else {
195                     value = s.Substring(si, i-si);
196                 }
197 
198                 // add name / value pair to the collection
199 
200                 if (urlencoded)
201                     base.Add(
202                        HttpUtility.UrlDecode(name, encoding),
203                        HttpUtility.UrlDecode(value, encoding));
204                 else
205                     base.Add(name, value);
206 
207                 // trailing '&'
208 
209                 if (i == l-1 && s[i] == '&')
210                     base.Add(null, String.Empty);
211 
212                 i++;
213             }
214         }
215 
FillFromEncodedBytes(byte[] bytes, Encoding encoding)216         internal void FillFromEncodedBytes(byte[] bytes, Encoding encoding) {
217             int l = (bytes != null) ? bytes.Length : 0;
218             int i = 0;
219 
220             while (i < l) {
221                 // find next & while noting first = on the way (and if there are more)
222 
223                 ThrowIfMaxHttpCollectionKeysExceeded();
224 
225                 int si = i;
226                 int ti = -1;
227 
228                 while (i < l) {
229                     byte b = bytes[i];
230 
231                     if (b == '=') {
232                         if (ti < 0)
233                             ti = i;
234                     }
235                     else if (b == '&') {
236                         break;
237                     }
238 
239                     i++;
240                 }
241 
242                 // extract the name / value pair
243 
244                 String name, value;
245 
246                 if (ti >= 0) {
247                     name  = HttpUtility.UrlDecode(bytes, si, ti-si, encoding);
248                     value = HttpUtility.UrlDecode(bytes, ti+1, i-ti-1, encoding);
249                 }
250                 else {
251                     name = null;
252                     value = HttpUtility.UrlDecode(bytes, si, i-si, encoding);
253                 }
254 
255                 // add name / value pair to the collection
256 
257                 base.Add(name, value);
258 
259                 // trailing '&'
260 
261                 if (i == l-1 && bytes[i] == '&')
262                     base.Add(null, String.Empty);
263 
264                 i++;
265             }
266         }
267 
Add(HttpCookieCollection c)268         internal void Add(HttpCookieCollection c) {
269             int n = c.Count;
270 
271             for (int i = 0; i < n; i++) {
272                 ThrowIfMaxHttpCollectionKeysExceeded();
273                 HttpCookie cookie = c.Get(i);
274                 base.Add(cookie.Name, cookie.Value);
275             }
276         }
277 
278         // MSRC 12038: limit the maximum number of items that can be added to the collection,
279         // as a large number of items potentially can result in too many hash collisions that may cause DoS
ThrowIfMaxHttpCollectionKeysExceeded()280         internal void ThrowIfMaxHttpCollectionKeysExceeded() {
281             if (base.Count >= AppSettings.MaxHttpCollectionKeys) {
282                 throw new InvalidOperationException(SR.GetString(SR.CollectionCountExceeded_HttpValueCollection, AppSettings.MaxHttpCollectionKeys));
283             }
284         }
285 
Reset()286         internal void Reset() {
287             base.Clear();
288         }
289 
ToString()290         public override String ToString() {
291             return ToString(true);
292         }
293 
ToString(bool urlencoded)294         internal virtual String ToString(bool urlencoded) {
295             return ToString(urlencoded, null);
296         }
297 
ToString(bool urlencoded, IDictionary excludeKeys)298         internal virtual String ToString(bool urlencoded, IDictionary excludeKeys) {
299             int n = Count;
300             if (n == 0)
301                 return String.Empty;
302 
303             StringBuilder s = new StringBuilder();
304             String key, keyPrefix, item;
305             bool ignoreViewStateKeys = (excludeKeys != null && excludeKeys[Page.ViewStateFieldPrefixID] != null);
306 
307             for (int i = 0; i < n; i++) {
308                 key = GetKey(i);
309 
310                 // Review: improve this... Special case hack for __VIEWSTATE#
311                 if (ignoreViewStateKeys && key != null && key.StartsWith(Page.ViewStateFieldPrefixID, StringComparison.Ordinal)) continue;
312                 if (excludeKeys != null && key != null && excludeKeys[key] != null)
313                     continue;
314                 if (urlencoded)
315                     key = UrlEncodeForToString(key);
316                 keyPrefix = (key != null) ? (key + "=") : String.Empty;
317 
318                 string[] values = GetValues(i);
319 
320                 if (s.Length > 0)
321                     s.Append('&');
322 
323                 if (values == null || values.Length == 0) {
324                     s.Append(keyPrefix);
325                 }
326                 else if (values.Length == 1) {
327                     s.Append(keyPrefix);
328                     item = values[0];
329                     if (urlencoded)
330                         item = UrlEncodeForToString(item);
331                     s.Append(item);
332                 }
333                 else {
334                     for (int j = 0; j < values.Length; j++) {
335                         if (j > 0)
336                             s.Append('&');
337                         s.Append(keyPrefix);
338                         item = values[j];
339                         if (urlencoded)
340                             item = UrlEncodeForToString(item);
341                         s.Append(item);
342                     }
343                 }
344             }
345 
346             return s.ToString();
347         }
348 
349         // HttpValueCollection used to call UrlEncodeUnicode in its ToString method, so we should continue to
350         // do so for back-compat. The result of ToString is not used to make a security decision, so this
351         // code path is "safe".
UrlEncodeForToString(string input)352         internal static string UrlEncodeForToString(string input) {
353             if (AppSettings.DontUsePercentUUrlEncoding) {
354                 // DevDiv #762975: <form action> and other similar URLs are mangled since we use non-standard %uXXXX encoding.
355                 // We need to use standard UTF8 encoding for modern browsers to understand the URLs.
356                 return HttpUtility.UrlEncode(input);
357             }
358             else {
359 #pragma warning disable 618 // [Obsolete]
360                 return HttpUtility.UrlEncodeUnicode(input);
361 #pragma warning restore 618
362             }
363         }
364 
365     }
366 
367 }
368