e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
368 lines
13 KiB
C#
368 lines
13 KiB
C#
//------------------------------------------------------------------------------
|
|
// <copyright file="HttpValueCollection.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
//------------------------------------------------------------------------------
|
|
|
|
/*
|
|
* Ordered String/String[] collection of name/value pairs
|
|
* Based on NameValueCollection -- adds parsing from string, cookie collection
|
|
*
|
|
* Copyright (c) 2000 Microsoft Corporation
|
|
*/
|
|
|
|
namespace System.Web {
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using System.Runtime.Serialization;
|
|
using System.Text;
|
|
using System.Web.UI;
|
|
using System.Web.Util;
|
|
|
|
[Serializable()]
|
|
internal class HttpValueCollection : NameValueCollection {
|
|
|
|
// for implementing granular request validation
|
|
[NonSerialized]
|
|
private ValidateStringCallback _validationCallback;
|
|
[NonSerialized]
|
|
private HashSet<string> _keysAwaitingValidation;
|
|
|
|
internal HttpValueCollection(): base(StringComparer.OrdinalIgnoreCase) {
|
|
}
|
|
|
|
// This copy constructor is used by the granular request validation feature. Since these collections are immutable
|
|
// once created, it's ok for us to have two collections containing the same data.
|
|
internal HttpValueCollection(HttpValueCollection col)
|
|
: base(StringComparer.OrdinalIgnoreCase) {
|
|
|
|
// We explicitly don't copy validation-related fields, as we want the copy to "reset" validation state. But we
|
|
// do need to go against the underlying NameObjectCollectionBase directly while copying so as to avoid triggering
|
|
// validation.
|
|
for (int i = 0; i < col.Count; i++) {
|
|
ThrowIfMaxHttpCollectionKeysExceeded();
|
|
string key = col.BaseGetKey(i);
|
|
object value = col.BaseGet(i);
|
|
BaseAdd(key, value);
|
|
}
|
|
|
|
IsReadOnly = col.IsReadOnly;
|
|
}
|
|
|
|
internal HttpValueCollection(String str, bool readOnly, bool urlencoded, Encoding encoding): base(StringComparer.OrdinalIgnoreCase) {
|
|
if (!String.IsNullOrEmpty(str))
|
|
FillFromString(str, urlencoded, encoding);
|
|
|
|
IsReadOnly = readOnly;
|
|
}
|
|
|
|
internal HttpValueCollection(int capacity) : base(capacity, StringComparer.OrdinalIgnoreCase) {
|
|
}
|
|
|
|
protected HttpValueCollection(SerializationInfo info, StreamingContext context) : base(info, context) {
|
|
}
|
|
|
|
/*
|
|
* We added granular request validation in ASP.NET 4.5 to provide a better request validation story for our developers.
|
|
* Instead of validating the entire collection ahead of time, we'll only validate entries that are actually looked at.
|
|
*/
|
|
|
|
internal void EnableGranularValidation(ValidateStringCallback validationCallback) {
|
|
// Iterate over all the keys, adding each to the set containing the keys awaiting validation.
|
|
// Unlike dictionaries, HashSet<T> can contain null keys, so don't need to special-case them.
|
|
_keysAwaitingValidation = new HashSet<string>(Keys.Cast<string>().Where(KeyIsCandidateForValidation), StringComparer.OrdinalIgnoreCase);
|
|
_validationCallback = validationCallback;
|
|
|
|
// This forces CopyTo and other methods that cache entries to flush their caches, ensuring
|
|
// that all values go through validation again.
|
|
InvalidateCachedArrays();
|
|
}
|
|
|
|
internal static bool KeyIsCandidateForValidation(string key) {
|
|
// Skip all our internal fields, since they don't need to be checked (VSWhidbey 275811)
|
|
//
|
|
|
|
if (key != null && key.StartsWith(System.Web.UI.Page.systemPostFieldPrefix, StringComparison.Ordinal)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void EnsureKeyValidated(string key) {
|
|
if (_keysAwaitingValidation == null) {
|
|
// If dynamic validation hasn't been enabled, no-op.
|
|
return;
|
|
}
|
|
|
|
if (!_keysAwaitingValidation.Contains(key)) {
|
|
// If this key has already been validated (or is excluded), no-op.
|
|
return;
|
|
}
|
|
|
|
// If validation fails, the callback will throw an exception. If validation succeeds,
|
|
// we can remove it from the candidates list. Two notes:
|
|
// - Use base.Get instead of this.Get so as not to enter infinite recursion.
|
|
// - Eager validation skips null/empty values, so we should, also.
|
|
string value = base.Get(key);
|
|
if (!String.IsNullOrEmpty(value)) {
|
|
_validationCallback(key, value);
|
|
}
|
|
_keysAwaitingValidation.Remove(key);
|
|
}
|
|
|
|
public override string Get(int index) {
|
|
// Need the key so that we can pass it through validation.
|
|
string key = GetKey(index);
|
|
EnsureKeyValidated(key);
|
|
|
|
return base.Get(index);
|
|
}
|
|
|
|
public override string Get(string name) {
|
|
EnsureKeyValidated(name);
|
|
return base.Get(name);
|
|
}
|
|
|
|
public override string[] GetValues(int index) {
|
|
// Need the key so that we can pass it through validation.
|
|
string key = GetKey(index);
|
|
EnsureKeyValidated(key);
|
|
|
|
return base.GetValues(index);
|
|
}
|
|
|
|
public override string[] GetValues(string name) {
|
|
EnsureKeyValidated(name);
|
|
return base.GetValues(name);
|
|
}
|
|
|
|
/*
|
|
* END REQUEST VALIDATION
|
|
*/
|
|
|
|
internal void MakeReadOnly() {
|
|
IsReadOnly = true;
|
|
}
|
|
|
|
internal void MakeReadWrite() {
|
|
IsReadOnly = false;
|
|
}
|
|
|
|
internal void FillFromString(String s) {
|
|
FillFromString(s, false, null);
|
|
}
|
|
|
|
internal void FillFromString(String s, bool urlencoded, Encoding encoding) {
|
|
int l = (s != null) ? s.Length : 0;
|
|
int i = 0;
|
|
|
|
while (i < l) {
|
|
// find next & while noting first = on the way (and if there are more)
|
|
|
|
ThrowIfMaxHttpCollectionKeysExceeded();
|
|
|
|
int si = i;
|
|
int ti = -1;
|
|
|
|
while (i < l) {
|
|
char ch = s[i];
|
|
|
|
if (ch == '=') {
|
|
if (ti < 0)
|
|
ti = i;
|
|
}
|
|
else if (ch == '&') {
|
|
break;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
// extract the name / value pair
|
|
|
|
String name = null;
|
|
String value = null;
|
|
|
|
if (ti >= 0) {
|
|
name = s.Substring(si, ti-si);
|
|
value = s.Substring(ti+1, i-ti-1);
|
|
}
|
|
else {
|
|
value = s.Substring(si, i-si);
|
|
}
|
|
|
|
// add name / value pair to the collection
|
|
|
|
if (urlencoded)
|
|
base.Add(
|
|
HttpUtility.UrlDecode(name, encoding),
|
|
HttpUtility.UrlDecode(value, encoding));
|
|
else
|
|
base.Add(name, value);
|
|
|
|
// trailing '&'
|
|
|
|
if (i == l-1 && s[i] == '&')
|
|
base.Add(null, String.Empty);
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
internal void FillFromEncodedBytes(byte[] bytes, Encoding encoding) {
|
|
int l = (bytes != null) ? bytes.Length : 0;
|
|
int i = 0;
|
|
|
|
while (i < l) {
|
|
// find next & while noting first = on the way (and if there are more)
|
|
|
|
ThrowIfMaxHttpCollectionKeysExceeded();
|
|
|
|
int si = i;
|
|
int ti = -1;
|
|
|
|
while (i < l) {
|
|
byte b = bytes[i];
|
|
|
|
if (b == '=') {
|
|
if (ti < 0)
|
|
ti = i;
|
|
}
|
|
else if (b == '&') {
|
|
break;
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
// extract the name / value pair
|
|
|
|
String name, value;
|
|
|
|
if (ti >= 0) {
|
|
name = HttpUtility.UrlDecode(bytes, si, ti-si, encoding);
|
|
value = HttpUtility.UrlDecode(bytes, ti+1, i-ti-1, encoding);
|
|
}
|
|
else {
|
|
name = null;
|
|
value = HttpUtility.UrlDecode(bytes, si, i-si, encoding);
|
|
}
|
|
|
|
// add name / value pair to the collection
|
|
|
|
base.Add(name, value);
|
|
|
|
// trailing '&'
|
|
|
|
if (i == l-1 && bytes[i] == '&')
|
|
base.Add(null, String.Empty);
|
|
|
|
i++;
|
|
}
|
|
}
|
|
|
|
internal void Add(HttpCookieCollection c) {
|
|
int n = c.Count;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
ThrowIfMaxHttpCollectionKeysExceeded();
|
|
HttpCookie cookie = c.Get(i);
|
|
base.Add(cookie.Name, cookie.Value);
|
|
}
|
|
}
|
|
|
|
// MSRC 12038: limit the maximum number of items that can be added to the collection,
|
|
// as a large number of items potentially can result in too many hash collisions that may cause DoS
|
|
internal void ThrowIfMaxHttpCollectionKeysExceeded() {
|
|
if (base.Count >= AppSettings.MaxHttpCollectionKeys) {
|
|
throw new InvalidOperationException(SR.GetString(SR.CollectionCountExceeded_HttpValueCollection, AppSettings.MaxHttpCollectionKeys));
|
|
}
|
|
}
|
|
|
|
internal void Reset() {
|
|
base.Clear();
|
|
}
|
|
|
|
public override String ToString() {
|
|
return ToString(true);
|
|
}
|
|
|
|
internal virtual String ToString(bool urlencoded) {
|
|
return ToString(urlencoded, null);
|
|
}
|
|
|
|
internal virtual String ToString(bool urlencoded, IDictionary excludeKeys) {
|
|
int n = Count;
|
|
if (n == 0)
|
|
return String.Empty;
|
|
|
|
StringBuilder s = new StringBuilder();
|
|
String key, keyPrefix, item;
|
|
bool ignoreViewStateKeys = (excludeKeys != null && excludeKeys[Page.ViewStateFieldPrefixID] != null);
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
key = GetKey(i);
|
|
|
|
// Review: improve this... Special case hack for __VIEWSTATE#
|
|
if (ignoreViewStateKeys && key != null && key.StartsWith(Page.ViewStateFieldPrefixID, StringComparison.Ordinal)) continue;
|
|
if (excludeKeys != null && key != null && excludeKeys[key] != null)
|
|
continue;
|
|
if (urlencoded)
|
|
key = UrlEncodeForToString(key);
|
|
keyPrefix = (key != null) ? (key + "=") : String.Empty;
|
|
|
|
string[] values = GetValues(i);
|
|
|
|
if (s.Length > 0)
|
|
s.Append('&');
|
|
|
|
if (values == null || values.Length == 0) {
|
|
s.Append(keyPrefix);
|
|
}
|
|
else if (values.Length == 1) {
|
|
s.Append(keyPrefix);
|
|
item = values[0];
|
|
if (urlencoded)
|
|
item = UrlEncodeForToString(item);
|
|
s.Append(item);
|
|
}
|
|
else {
|
|
for (int j = 0; j < values.Length; j++) {
|
|
if (j > 0)
|
|
s.Append('&');
|
|
s.Append(keyPrefix);
|
|
item = values[j];
|
|
if (urlencoded)
|
|
item = UrlEncodeForToString(item);
|
|
s.Append(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
return s.ToString();
|
|
}
|
|
|
|
// HttpValueCollection used to call UrlEncodeUnicode in its ToString method, so we should continue to
|
|
// do so for back-compat. The result of ToString is not used to make a security decision, so this
|
|
// code path is "safe".
|
|
internal static string UrlEncodeForToString(string input) {
|
|
if (AppSettings.DontUsePercentUUrlEncoding) {
|
|
// DevDiv #762975: <form action> and other similar URLs are mangled since we use non-standard %uXXXX encoding.
|
|
// We need to use standard UTF8 encoding for modern browsers to understand the URLs.
|
|
return HttpUtility.UrlEncode(input);
|
|
}
|
|
else {
|
|
#pragma warning disable 618 // [Obsolete]
|
|
return HttpUtility.UrlEncodeUnicode(input);
|
|
#pragma warning restore 618
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}
|