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