1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-08 06:28:14 -05:00
bitwarden/src/Core/Utilities/JsonHelpers.cs
Rui Tomé 9d59e4dc9e
[AC-1637] Sanitize Business and Organization Names from html script injection prior to storing in db (#3302)
* [AC-1637] Added HtmlEncodingStringConverter to encode/decode special chars on JSON serialization/deserialization

* [AC-1637] Added unit tests for HtmlEncodingStringConverter

* [AC-1637] Moved expected values on unit tests to the arrange phase

* [AC-1637] Added HtmlEncodingStringConverter to properties that are for input/output of Org Name and Business name

* [AC-1637] Modified views in Admin project to decode values to display

* [AC-1637] Replaced Html.Raw with HttpUtility.HtmlDecode

* [AC-1637] Added JsonConverter to Provider DTOs

* [AC-1637] Modified HandlebarsMailService to decode organization name before sending emails

* Revert "[AC-1637] Added JsonConverter to Provider DTOs"

This reverts commit 94d507cf93e4c9f7f02890b9286dba90bad3f516.

* [AC-1637] Fixed Admin panel organization search

* [AC-1637] Sanitizing Organization name and business name on creation in Admin panel

* [AC-1637] Sanitizing organization name and business name on creation by a provider

* [AC-1637] Sanitizing provider name on creation and on viewing in admin panel

* [AC-1637] Added sanitization to more places where Org name is used

* [AC-1637] Swapped using HttpUtility for WebUtility since the later is part of the dotnet framework

* [AC-1637] Updated error messages

* [AC-1637] Decoding on Admin panel add existing organization

* [AC-1637] Fix HTML decoding issues

* [AC-1637] Refactor HTML decoding in View and Model classes on Admin panel

* [AC-1637] Refactor provider name and business name usages to use methods that output decoded values

* [AC-1637] Fixed typo

* [AC-1637] Renamed Provider methods to retrieve Decoded Name and BusinessName

* [AC-1637] Renamed Organization methods to retrieve Decoded Name and BusinessName

* [AC-1637] Update the display name method in the `ProviderOrganizationOrganizationDetails` class to `DisplayName()`
2024-03-05 10:56:48 +00:00

226 lines
7.1 KiB
C#

using System.Globalization;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using NS = Newtonsoft.Json;
namespace Bit.Core.Utilities;
public static class JsonHelpers
{
public static JsonSerializerOptions Default { get; }
public static JsonSerializerOptions Indented { get; }
public static JsonSerializerOptions IgnoreCase { get; }
public static JsonSerializerOptions IgnoreWritingNull { get; }
public static JsonSerializerOptions CamelCase { get; }
public static JsonSerializerOptions IgnoreWritingNullAndCamelCase { get; }
static JsonHelpers()
{
Default = new JsonSerializerOptions();
Indented = new JsonSerializerOptions
{
WriteIndented = true,
};
IgnoreCase = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
IgnoreWritingNull = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
CamelCase = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
IgnoreWritingNullAndCamelCase = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
}
public static T DeserializeOrNew<T>(string json, JsonSerializerOptions options = null)
where T : new()
{
if (string.IsNullOrWhiteSpace(json))
{
return new T();
}
return JsonSerializer.Deserialize<T>(json, options);
}
#region Legacy Newtonsoft.Json usage
private const string LegacyMessage = "Usage of Newtonsoft.Json should be kept to a minimum and will further be removed when we move to .NET 6";
[Obsolete(LegacyMessage)]
public static NS.JsonSerializerSettings LegacyEnumKeyResolver { get; } = new NS.JsonSerializerSettings
{
ContractResolver = new EnumKeyResolver<byte>(),
};
[Obsolete(LegacyMessage)]
public static string LegacySerialize(object value, NS.JsonSerializerSettings settings = null)
{
return NS.JsonConvert.SerializeObject(value, settings);
}
[Obsolete(LegacyMessage)]
public static T LegacyDeserialize<T>(string value, NS.JsonSerializerSettings settings = null)
{
return NS.JsonConvert.DeserializeObject<T>(value, settings);
}
#endregion
}
public class EnumKeyResolver<T> : NS.Serialization.DefaultContractResolver
where T : struct
{
protected override NS.Serialization.JsonDictionaryContract CreateDictionaryContract(Type objectType)
{
var contract = base.CreateDictionaryContract(objectType);
var keyType = contract.DictionaryKeyType;
if (keyType.BaseType == typeof(Enum))
{
contract.DictionaryKeyResolver = propName => ((T)Enum.Parse(keyType, propName)).ToString();
}
return contract;
}
}
public class MsEpochConverter : JsonConverter<DateTime?>
{
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (!long.TryParse(reader.GetString(), out var milliseconds))
{
return null;
}
return CoreHelpers.FromEpocMilliseconds(milliseconds);
}
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (!value.HasValue)
{
writer.WriteNullValue();
}
writer.WriteStringValue(CoreHelpers.ToEpocMilliseconds(value.Value).ToString());
}
}
/// <summary>
/// Allows reading a string from a JSON number or string, should only be used on <see cref="string" /> properties
/// </summary>
public class PermissiveStringConverter : JsonConverter<string>
{
internal static readonly PermissiveStringConverter Instance = new();
private static readonly CultureInfo _cultureInfo = new("en-US");
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal().ToString(_cultureInfo),
JsonTokenType.True => bool.TrueString,
JsonTokenType.False => bool.FalseString,
_ => throw new JsonException($"Unsupported TokenType: {reader.TokenType}"),
};
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
/// <summary>
/// Allows reading a JSON array of number or string, should only be used on <see cref="IEnumerable{T}" /> whose generic type is <see cref="string" />
/// </summary>
public class PermissiveStringEnumerableConverter : JsonConverter<IEnumerable<string>>
{
public override IEnumerable<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var stringList = new List<string>();
// Handle special cases or throw
if (reader.TokenType != JsonTokenType.StartArray)
{
// An array was expected but to be extra permissive allow reading from anything other than an object
if (reader.TokenType == JsonTokenType.StartObject)
{
throw new JsonException("Cannot read JSON Object to an IEnumerable<string>.");
}
stringList.Add(PermissiveStringConverter.Instance.Read(ref reader, typeof(string), options));
return stringList;
}
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
stringList.Add(PermissiveStringConverter.Instance.Read(ref reader, typeof(string), options));
}
return stringList;
}
public override void Write(Utf8JsonWriter writer, IEnumerable<string> value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var str in value)
{
PermissiveStringConverter.Instance.Write(writer, str, options);
}
writer.WriteEndArray();
}
}
/// <summary>
/// Encodes incoming strings using HTML encoding
/// and decodes outgoing strings using HTML decoding.
/// </summary>
public class HtmlEncodingStringConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var originalValue = reader.GetString();
return WebUtility.HtmlEncode(originalValue);
}
return reader.GetString();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (!string.IsNullOrEmpty(value))
{
var encodedValue = WebUtility.HtmlDecode(value);
writer.WriteStringValue(encodedValue);
}
else
{
writer.WriteNullValue();
}
}
}