Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Microsoft.Azure.Cosmos/src/CosmosElements/CosmosArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.CosmosElements
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.Azure.Cosmos.Json;
using Microsoft.Azure.Cosmos.Query.Core.Monads;
using Microsoft.Azure.Cosmos.Query.Core.Pipeline.Distinct;
Expand Down Expand Up @@ -46,6 +47,11 @@ protected CosmosArray()

public bool Equals(CosmosArray cosmosArray)
{
// Guard against stack exhaustion on deeply-nested input. Converts an
// otherwise-uncatchable StackOverflowException into a catchable
// InsufficientExecutionStackException.
RuntimeHelpers.EnsureSufficientExecutionStack();

if (this.Count != cosmosArray.Count)
{
return false;
Expand All @@ -65,6 +71,11 @@ public bool Equals(CosmosArray cosmosArray)

public override int GetHashCode()
{
// Guard against stack exhaustion on deeply-nested input. Converts an
// otherwise-uncatchable StackOverflowException into a catchable
// InsufficientExecutionStackException.
RuntimeHelpers.EnsureSufficientExecutionStack();

uint hash = HashSeed;

// Incorporate all the array items into the hash.
Expand Down
11 changes: 11 additions & 0 deletions Microsoft.Azure.Cosmos/src/CosmosElements/CosmosObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.Azure.Cosmos.CosmosElements
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Microsoft.Azure.Cosmos.Json;
using Microsoft.Azure.Cosmos.Query.Core.Monads;
using Microsoft.Azure.Cosmos.Query.Core.Pipeline.Distinct;
Expand Down Expand Up @@ -97,6 +98,11 @@ public override bool Equals(CosmosElement cosmosElement)

public bool Equals(CosmosObject cosmosObject)
{
// Guard against stack exhaustion on deeply-nested input. Converts an
// otherwise-uncatchable StackOverflowException into a catchable
// InsufficientExecutionStackException.
RuntimeHelpers.EnsureSufficientExecutionStack();

if (this.Count != cosmosObject.Count)
{
return false;
Expand Down Expand Up @@ -124,6 +130,11 @@ public bool Equals(CosmosObject cosmosObject)

public override int GetHashCode()
{
// Guard against stack exhaustion on deeply-nested input. Converts an
// otherwise-uncatchable StackOverflowException into a catchable
// InsufficientExecutionStackException.
RuntimeHelpers.EnsureSufficientExecutionStack();

uint hash = HashSeed;
foreach (KeyValuePair<string, CosmosElement> kvp in this)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,22 @@ private static class ValueLengths

public static long GetValueLength(ReadOnlySpan<byte> buffer)
{
return GetValueLength(buffer, depth: 0);
}

private static long GetValueLength(ReadOnlySpan<byte> buffer, int depth)
{
// Guard against malicious payloads that nest Arr1 (0xE1) / Obj1 (0xE9)
// type markers deeply enough to exhaust the CLR stack. The cap matches
// the streaming reader's JsonObjectState.JsonMaxNestingDepth so the
// binary navigator path enforces the same nesting policy as the rest
// of the JSON stack. Both paths allow exactly N = 256 levels before
// throwing JsonMaxNestingExceededException.
if (depth >= JsonObjectState.JsonMaxNestingDepth)
{
throw new JsonMaxNestingExceededException();
}

long length = ValueLengths.Lookup[buffer[0]];
if (length < 0)
{
Expand Down Expand Up @@ -215,19 +231,19 @@ public static long GetValueLength(ReadOnlySpan<byte> buffer)
break;

case Arr1:
long arrayOneItemLength = ValueLengths.GetValueLength(buffer.Slice(1));
long arrayOneItemLength = ValueLengths.GetValueLength(buffer.Slice(1), depth + 1);
length = arrayOneItemLength == 0 ? 0 : 1 + arrayOneItemLength;
break;

case Obj1:
long nameLength = ValueLengths.GetValueLength(buffer.Slice(1));
long nameLength = ValueLengths.GetValueLength(buffer.Slice(1), depth + 1);
if (nameLength == 0)
{
length = 0;
}
else
{
long valueLength = ValueLengths.GetValueLength(buffer.Slice(1 + (int)nameLength));
long valueLength = ValueLengths.GetValueLength(buffer.Slice(1 + (int)nameLength), depth + 1);
length = TypeMarkerLength + nameLength + valueLength;
}
break;
Expand Down
25 changes: 21 additions & 4 deletions Microsoft.Azure.Cosmos/src/Json/JsonBinaryEncoding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,29 @@ public static int GetFirstValueOffset(byte typeMarker)
return JsonBinaryEncoding.FirstValueOffsets.Offsets[typeMarker];
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryGetValueLength(ReadOnlySpan<byte> buffer, out int length)
{
// Too lazy to convert this right now.
length = JsonBinaryEncoding.GetValueLength(buffer);
return true;
// Honor the Try-pattern contract: any malformed payload -- whether
// it trips the nesting cap (JsonParseException), is empty
// (IndexOutOfRangeException), has a length prefix that runs past
// the buffer (ArgumentOutOfRangeException), exceeds int.MaxValue
// (InvalidOperationException), or carries an unknown type marker
// (ArgumentException) -- must surface as a false return rather
// than leaking an exception to the caller. GetValueLength is a
// pure function with no side effects, so swallowing here is safe.
// [AggressiveInlining] is intentionally omitted: methods that
// contain a try/catch cannot be inlined by the JIT, so the hint
// would be misleading.
try
{
length = JsonBinaryEncoding.GetValueLength(buffer);
return true;
}
catch (Exception)
{
length = 0;
return false;
}
}

private static bool TryGetFixedWidthValue<T>(
Expand Down
5 changes: 5 additions & 0 deletions Microsoft.Azure.Cosmos/src/Json/JsonNavigator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ protected JsonNavigator()
/// <param name="buffer">The buffer to navigate</param>
/// <param name="jsonStringDictionary">The optional json string dictionary for binary encoding.</param>
/// <returns>A concrete JsonNavigator that can navigate the supplied buffer.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="buffer"/> is empty.</exception>
/// <exception cref="JsonMaxNestingExceededException">
/// Thrown when a binary-format buffer nests array/object type markers more deeply than the parser allows.
/// This guards against crafted payloads that would otherwise exhaust the call stack.
/// </exception>
public static IJsonNavigator Create(
ReadOnlyMemory<byte> buffer,
IJsonStringDictionary jsonStringDictionary = null)
Expand Down
8 changes: 6 additions & 2 deletions Microsoft.Azure.Cosmos/src/Json/JsonObjectState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ sealed class JsonObjectState
/// FWIW .Net chose 100
/// Note: This value needs to be a multiple of 8 and must be less than 2^15 (see asserts in the constructor)
/// </summary>
private const int JsonMaxNestingDepth = 256;
internal const int JsonMaxNestingDepth = 256;

/// <summary>
/// Flag for determining whether to throw exceptions that connote a context at the end or not started / complete.
Expand Down Expand Up @@ -181,7 +181,11 @@ private void Push(bool isArray)
{
if (this.nestingStackIndex + 1 >= JsonMaxNestingDepth)
{
throw new InvalidOperationException(RMResources.JsonMaxNestingExceeded);
// Use the dedicated JsonMaxNestingExceededException so callers can
// catch the "JSON too deeply nested" condition with a single type
// regardless of whether the limit was hit by the streaming reader
// or by the binary value-length decoder.
throw new JsonMaxNestingExceededException();
}

this.nestingStackIndex++;
Expand Down
10 changes: 10 additions & 0 deletions Microsoft.Azure.Cosmos/src/Json/JsonReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ protected JsonReader()
/// <param name="buffer">The byte array (with format marker) to read from.</param>
/// <param name="jsonStringDictionary">The dictionary to use for user string encoding.</param>
/// <returns>A concrete JsonReader that can read the supplied byte array.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="buffer"/> is empty.</exception>
/// <exception cref="JsonMaxNestingExceededException">
/// Thrown when a binary-format buffer nests array/object type markers more deeply than the parser allows.
/// This guards against crafted payloads that would otherwise exhaust the call stack.
/// </exception>
public static IJsonReader Create(ReadOnlyMemory<byte> buffer, IJsonStringDictionary jsonStringDictionary = null)
{
if (buffer.IsEmpty)
Expand All @@ -67,6 +72,11 @@ public static IJsonReader Create(ReadOnlyMemory<byte> buffer, IJsonStringDiction
/// <param name="buffer">The buffer to read from.</param>
/// <param name="jsonStringDictionary">The optional dictionary to decode strings.</param>
/// <returns>An <see cref="IJsonReader"/> for the buffer, format, and dictionary.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="buffer"/> is empty or <paramref name="jsonSerializationFormat"/> is not recognized.</exception>
/// <exception cref="JsonMaxNestingExceededException">
/// Thrown for binary-format buffers that nest array/object type markers more deeply than the parser allows.
/// This guards against crafted payloads that would otherwise exhaust the call stack.
/// </exception>
public static IJsonReader Create(
JsonSerializationFormat jsonSerializationFormat,
ReadOnlyMemory<byte> buffer,
Expand Down
13 changes: 13 additions & 0 deletions Microsoft.Azure.Cosmos/src/Json/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.Json
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Azure.Cosmos.CosmosElements;
using Microsoft.Azure.Cosmos.Query.Core.Monads;

Expand Down Expand Up @@ -217,6 +218,12 @@ private DeserializationVisitor()

public TryCatch<object> Visit(CosmosArray cosmosArray, Type type)
{
// Recursive walk over a materialized CosmosElement graph: guard
// against a hostile deeply-nested response payload exhausting
// the CLR stack and crashing the host with an unrecoverable
// StackOverflowException.
RuntimeHelpers.EnsureSufficientExecutionStack();

bool isReadOnlyList = type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(IReadOnlyList<>));
if (!isReadOnlyList)
{
Expand Down Expand Up @@ -503,6 +510,12 @@ public TryCatch<object> Visit(CosmosNumber cosmosNumber, Type type)

public TryCatch<object> Visit(CosmosObject cosmosObject, Type type)
{
// Recursive walk over a materialized CosmosElement graph: guard
// against a hostile deeply-nested response payload exhausting
// the CLR stack and crashing the host with an unrecoverable
// StackOverflowException.
RuntimeHelpers.EnsureSufficientExecutionStack();

ConstructorInfo[] constructors = type.GetConstructors();
if (constructors.Length == 0)
{
Expand Down
87 changes: 79 additions & 8 deletions Microsoft.Azure.Cosmos/src/Json/JsonWriter.JsonBinaryWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Cosmos.Json
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.Azure.Cosmos.Core;
using Microsoft.Azure.Cosmos.Core.Utf8;
Expand Down Expand Up @@ -1640,6 +1641,17 @@ private void ForceRewriteRawJsonValue(
bool isFieldName,
IJsonStringDictionary jsonStringDictionary)
{
// Defense in depth against stack exhaustion on attacker-supplied
// buffers. The StrR1-4 reference-string cases below recurse via
// a byte offset read from the buffer itself, and the structural
// Arr / Obj cases recurse per element; both are bounded by
// RewriteResolvedReferenceString (reference invariant) and
// JsonObjectState.Push (256 nesting depth) respectively, but
// EnsureSufficientExecutionStack converts any escape from those
// checks into a catchable InsufficientExecutionStackException
// rather than an unrecoverable StackOverflowException.
RuntimeHelpers.EnsureSufficientExecutionStack();

ReadOnlyMemory<byte> rawJsonValue = rootBuffer.Slice(valueOffset);
byte typeMarker = rawJsonValue.Span[0];

Expand Down Expand Up @@ -1698,34 +1710,30 @@ private void ForceRewriteRawJsonValue(
break;

case RawValueType.StrR1:
this.ForceRewriteRawJsonValue(
this.RewriteResolvedReferenceString(
rootBuffer,
JsonBinaryEncoding.GetFixedSizedValue<byte>(rawJsonValue.Slice(start: 1).Span),
default,
isFieldName,
jsonStringDictionary);
break;
case RawValueType.StrR2:
this.ForceRewriteRawJsonValue(
this.RewriteResolvedReferenceString(
rootBuffer,
JsonBinaryEncoding.GetFixedSizedValue<ushort>(rawJsonValue.Slice(start: 1).Span),
default,
isFieldName,
jsonStringDictionary);
break;
case RawValueType.StrR3:
this.ForceRewriteRawJsonValue(
this.RewriteResolvedReferenceString(
rootBuffer,
JsonBinaryEncoding.GetFixedSizedValue<JsonBinaryEncoding.UInt24>(rawJsonValue.Slice(start: 1).Span),
default,
isFieldName,
jsonStringDictionary);
break;
case RawValueType.StrR4:
this.ForceRewriteRawJsonValue(
this.RewriteResolvedReferenceString(
rootBuffer,
JsonBinaryEncoding.GetFixedSizedValue<int>(rawJsonValue.Slice(start: 1).Span),
default,
isFieldName,
jsonStringDictionary);
break;
Expand Down Expand Up @@ -1833,6 +1841,69 @@ private void ForceRewriteRawJsonValue(
}
}

/// <summary>
/// Rewrites a reference string (StrR1/2/3/4) by validating that the
/// target offset is in-bounds and does not itself point at another
/// reference string, then delegating to
/// <see cref="ForceRewriteRawJsonValue"/>.
/// </summary>
/// <remarks>
/// The binary writer's invariant (enforced by
/// <see cref="FixReferenceStringOffsets"/>) is that every reference
/// string in a well-formed buffer points to a single non-reference
/// string value (StrL1/2/4, StrEncLen, StrUsr, StrBase64). In a
/// well-formed buffer the dereference is always exactly one hop deep.
///
/// On a hostile / corrupted buffer the target byte could be another
/// StrR marker, which would let an attacker construct an arbitrarily
/// deep chain or a cycle (e.g. StrR1@a -> StrR1@b -> StrR1@a) that
/// would otherwise drive <see cref="ForceRewriteRawJsonValue"/> into
/// an unrecoverable <see cref="StackOverflowException"/>. This method
/// rejects reference-to-reference at the precise byte where the
/// malformation occurs with a catchable
/// <see cref="JsonInvalidTokenException"/>, which is strictly cheaper
/// than relying on a depth counter and handles cycles of any size.
///
/// The unsigned bounds check also catches negative offsets (StrR4
/// reads a signed <c>int</c>) so the caller cannot index before the
/// start of the root buffer. Targets that are not string literals
/// (including other StrR markers) are rejected directly by this
/// helper, matching the writer-side invariant in
/// <see cref="FixReferenceStringOffsets"/>.
/// </remarks>
private void RewriteResolvedReferenceString(
ReadOnlyMemory<byte> rootBuffer,
int targetOffset,
bool isFieldName,
IJsonStringDictionary jsonStringDictionary)
{
if ((uint)targetOffset >= (uint)rootBuffer.Length)
{
throw new JsonInvalidTokenException();
}

byte targetTypeMarker = rootBuffer.Span[targetOffset];
if (!JsonBinaryEncoding.TypeMarker.IsString(targetTypeMarker)
|| JsonBinaryEncoding.TypeMarker.IsReferenceString(targetTypeMarker))
{
// Writer invariant (see FixReferenceStringOffsets): an StrR
// marker always resolves to a string literal in exactly one
// hop. Anything else (another reference, an array/object
// marker, a number, etc.) is malformed and -- left alone --
// would let a hostile buffer build cycles or non-string
// targets that recurse through ForceRewriteRawJsonValue.
// Reject up front.
throw new JsonInvalidTokenException();
}

this.ForceRewriteRawJsonValue(
rootBuffer,
targetOffset,
externalArrayInfo: default,
isFieldName: isFieldName,
jsonStringDictionary: jsonStringDictionary);
}

private void WriteRawStringValue(RawValueType rawValueType, ReadOnlyMemory<byte> buffer, bool isFieldName, IJsonStringDictionary jsonStringDictionary)
{
Utf8Span rawStringValue;
Expand Down
Loading
Loading