101 lines
5 KiB
C#
101 lines
5 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using MessagePack;
|
|
|
|
namespace GameCult.Caching.MessagePack;
|
|
|
|
internal enum PayloadDirection { Read, Write }
|
|
|
|
/// <summary>
|
|
/// The one walk of a payload's shape: its depth and its one value. Walks the MessagePack headers of a document payload iteratively, counting every array and
|
|
/// map (read by a formatter or skipped past it; the outermost array is depth 1) and refusing the first container past
|
|
/// <see cref="CultMessagePackSecurity.MaximumPayloadDepth"/>. Scalars are skipped by length, nothing is allocated per
|
|
/// element, and the count stack is bounded by the limit. A refusal names the type, the depth reached and the limit,
|
|
/// never payload bytes. A payload that ends early is the deserializer's to refuse; bytes after the first value are refused
|
|
/// with their count and never walked.
|
|
/// </summary>
|
|
internal static class CultPayloadDepth
|
|
{
|
|
public static void Check(ReadOnlySpan<byte> bytes, Type type, PayloadDirection direction)
|
|
{
|
|
const int limit = CultMessagePackSecurity.MaximumPayloadDepth;
|
|
Span<long> remaining = stackalloc long[limit];
|
|
var depth = 0;
|
|
long position = 0;
|
|
while (position < bytes.Length)
|
|
{
|
|
var code = bytes[(int)position];
|
|
long children = 0;
|
|
long header = 0;
|
|
long skip = 0;
|
|
var container = false;
|
|
switch (code)
|
|
{
|
|
case <= 0x7f or >= 0xe0 or 0xc0 or 0xc1 or 0xc2 or 0xc3: skip = 1; break;
|
|
case <= 0x8f: container = true; header = 1; children = 2L * (code & 0x0f); break;
|
|
case <= 0x9f: container = true; header = 1; children = code & 0x0f; break;
|
|
case <= 0xbf: skip = 1 + (code & 0x1f); break;
|
|
case 0xc4 or 0xd9: if (!Length(bytes, position, 1, out var l8)) return; skip = 2 + l8; break;
|
|
case 0xc5 or 0xda: if (!Length(bytes, position, 2, out var l16)) return; skip = 3 + l16; break;
|
|
case 0xc6 or 0xdb: if (!Length(bytes, position, 4, out var l32)) return; skip = 5 + l32; break;
|
|
case 0xc7: if (!Length(bytes, position, 1, out var e8)) return; skip = 3 + e8; break;
|
|
case 0xc8: if (!Length(bytes, position, 2, out var e16)) return; skip = 4 + e16; break;
|
|
case 0xc9: if (!Length(bytes, position, 4, out var e32)) return; skip = 6 + e32; break;
|
|
case 0xca or 0xce or 0xd2: skip = 5; break;
|
|
case 0xcb or 0xcf or 0xd3: skip = 9; break;
|
|
case 0xcc or 0xd0: skip = 2; break;
|
|
case 0xcd or 0xd1 or 0xd4: skip = 3; break;
|
|
case 0xd5: skip = 4; break;
|
|
case 0xd6: skip = 6; break;
|
|
case 0xd7: skip = 10; break;
|
|
case 0xd8: skip = 18; break;
|
|
case 0xdc: if (!Length(bytes, position, 2, out var a16)) return; container = true; header = 3; children = a16; break;
|
|
case 0xdd: if (!Length(bytes, position, 4, out var a32)) return; container = true; header = 5; children = a32; break;
|
|
case 0xde: if (!Length(bytes, position, 2, out var m16)) return; container = true; header = 3; children = 2 * m16; break;
|
|
default: if (!Length(bytes, position, 4, out var m32)) return; container = true; header = 5; children = 2 * m32; break;
|
|
}
|
|
|
|
if (container)
|
|
{
|
|
if (depth == limit)
|
|
throw new MessagePackSerializationException(
|
|
$"{type.FullName} payload nests {limit + 1} MessagePack containers deep; the limit is {limit} (refused on {(direction == PayloadDirection.Read ? "read" : "write")}).");
|
|
position += header;
|
|
if (children > 0)
|
|
{
|
|
remaining[depth++] = children;
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
position += skip;
|
|
}
|
|
|
|
// One element is complete; close every container it was the last element of.
|
|
while (depth > 0 && --remaining[depth - 1] == 0) depth--;
|
|
if (depth == 0)
|
|
{
|
|
if (position < bytes.Length)
|
|
throw new MessagePackSerializationException(
|
|
$"{type.FullName} payload has {bytes.Length - position} bytes after its first MessagePack value (refused on {(direction == PayloadDirection.Read ? "read" : "write")}).");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The big-endian length that follows the code byte, when the payload is long enough to hold it.
|
|
private static bool Length(ReadOnlySpan<byte> bytes, long position, int width, out long length)
|
|
{
|
|
length = 0;
|
|
if (position + 1 + width > bytes.Length) return false;
|
|
var field = bytes.Slice((int)position + 1, width);
|
|
length = width switch
|
|
{
|
|
1 => field[0],
|
|
2 => BinaryPrimitives.ReadUInt16BigEndian(field),
|
|
_ => BinaryPrimitives.ReadUInt32BigEndian(field)
|
|
};
|
|
return true;
|
|
}
|
|
}
|