Files
ft/Client/Packages/io.github.hatayama.uloopmcp/Editor/MetadataValidation/PreloadIlSecurityValidator.cs

565 lines
21 KiB
C#

using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Reflection.Emit;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Reads IL directly from PE metadata before Assembly.Load so restricted-mode validation
/// never needs to materialize the assembly just to inspect method bodies.
/// </summary>
public sealed class PreloadIlSecurityValidator : IPreloadAssemblySecurityValidator
{
private const char DecodedTypeNameSeparator = '|';
private static readonly OpCode[] SingleByteOpCodes = BuildOpCodeTable(singleByte: true);
private static readonly OpCode[] MultiByteOpCodes = BuildOpCodeTable(singleByte: false);
public SecurityValidationResult Validate(byte[] assemblyBytes)
{
SecurityValidationResult result = new SecurityValidationResult
{
IsValid = true,
Violations = new List<SecurityViolation>()
};
HashSet<string> seenViolations = new HashSet<string>(System.StringComparer.Ordinal);
using MemoryStream stream = new MemoryStream(assemblyBytes, writable: false);
using PEReader peReader = new PEReader(stream);
MetadataReader reader = peReader.GetMetadataReader();
foreach (MethodDefinitionHandle methodHandle in reader.MethodDefinitions)
{
this.ValidateMethod(peReader, reader, methodHandle, result, seenViolations);
}
result.IsValid = result.Violations.Count == 0;
return result;
}
private void ValidateMethod(
PEReader peReader,
MetadataReader reader,
MethodDefinitionHandle methodHandle,
SecurityValidationResult result,
HashSet<string> seenViolations)
{
MethodDefinition methodDefinition = reader.GetMethodDefinition(methodHandle);
string declaringTypeName = this.GetTypeDefinitionFullName(reader, methodDefinition.GetDeclaringType());
string methodName = reader.GetString(methodDefinition.Name);
this.ValidateParameterTypes(reader, methodDefinition, declaringTypeName, methodName, result, seenViolations);
this.ValidateMethodBody(peReader, reader, methodDefinition, declaringTypeName, methodName, result, seenViolations);
}
private void ValidateParameterTypes(
MetadataReader reader,
MethodDefinition methodDefinition,
string declaringTypeName,
string methodName,
SecurityValidationResult result,
HashSet<string> seenViolations)
{
MetadataTypeNameDecoder decoder = new MetadataTypeNameDecoder(reader, this);
MethodSignature<string> signature = methodDefinition.DecodeSignature(decoder, genericContext: null);
foreach (string parameterTypeName in signature.ParameterTypes)
{
this.AddDecodedTypeViolations(
result,
seenViolations,
parameterTypeName,
dangerousTypeName => $"Dangerous parameter type detected before assembly load: {dangerousTypeName}",
dangerousTypeName => $"Parameter type '{dangerousTypeName}' exists in {declaringTypeName}.{methodName}");
}
this.AddDecodedTypeViolations(
result,
seenViolations,
signature.ReturnType,
dangerousTypeName => $"Dangerous return type detected before assembly load: {dangerousTypeName}",
dangerousTypeName => $"Return type '{dangerousTypeName}' exists in {declaringTypeName}.{methodName}");
}
private void ValidateMethodBody(
PEReader peReader,
MetadataReader reader,
MethodDefinition methodDefinition,
string declaringTypeName,
string methodName,
SecurityValidationResult result,
HashSet<string> seenViolations)
{
if (methodDefinition.RelativeVirtualAddress == 0)
{
return;
}
MethodBodyBlock methodBody = peReader.GetMethodBody(methodDefinition.RelativeVirtualAddress);
this.ValidateLocalVariableTypes(reader, methodBody, declaringTypeName, methodName, result, seenViolations);
this.ValidateIlInstructions(reader, methodBody, declaringTypeName, methodName, result, seenViolations);
}
private void ValidateLocalVariableTypes(
MetadataReader reader,
MethodBodyBlock methodBody,
string declaringTypeName,
string methodName,
SecurityValidationResult result,
HashSet<string> seenViolations)
{
if (methodBody.LocalSignature.IsNil)
{
return;
}
StandaloneSignature standaloneSignature = reader.GetStandaloneSignature(methodBody.LocalSignature);
MetadataTypeNameDecoder decoder = new MetadataTypeNameDecoder(reader, this);
ImmutableArray<string> localTypes = standaloneSignature.DecodeLocalSignature(decoder, genericContext: null);
foreach (string localTypeName in localTypes)
{
this.AddDecodedTypeViolations(
result,
seenViolations,
localTypeName,
dangerousTypeName => $"Dangerous local variable type detected before assembly load: {dangerousTypeName}",
dangerousTypeName => $"Local variable type '{dangerousTypeName}' exists in {declaringTypeName}.{methodName}");
}
}
private void ValidateIlInstructions(
MetadataReader reader,
MethodBodyBlock methodBody,
string declaringTypeName,
string methodName,
SecurityValidationResult result,
HashSet<string> seenViolations)
{
byte[] ilBytes = methodBody.GetILBytes();
int offset = 0;
while (offset < ilBytes.Length)
{
OpCode opCode = ReadOpCode(ilBytes, ref offset);
if (opCode.OperandType == OperandType.InlineMethod)
{
int token = ReadInt32(ilBytes, offset);
EntityHandle methodHandle = MetadataTokens.EntityHandle(token);
if (this.TryGetMethodReference(methodHandle, reader, out string targetTypeName, out string targetMethodName) &&
DangerousApiCatalog.IsDangerousApi(targetTypeName, targetMethodName))
{
string apiName = $"{targetTypeName}.{targetMethodName}";
this.AddViolation(
result,
seenViolations,
apiName,
$"Dangerous IL method reference detected before assembly load: {apiName}",
$"IL {opCode.Name} references '{apiName}' in {declaringTypeName}.{methodName}");
}
}
if (opCode == OpCodes.Ldtoken && opCode.OperandType == OperandType.InlineTok)
{
int token = ReadInt32(ilBytes, offset);
string referencedTypeName = this.TryGetTypeReference(MetadataTokens.EntityHandle(token), reader);
this.AddDecodedTypeViolations(
result,
seenViolations,
referencedTypeName,
dangerousTypeName => $"Dangerous IL type token detected before assembly load: {dangerousTypeName}",
dangerousTypeName => $"IL ldtoken references '{dangerousTypeName}' in {declaringTypeName}.{methodName}");
}
offset += GetOperandSize(opCode.OperandType, ilBytes, offset);
}
}
private bool TryGetMethodReference(
EntityHandle handle,
MetadataReader reader,
out string typeName,
out string methodName)
{
typeName = string.Empty;
methodName = string.Empty;
if (handle.Kind == HandleKind.MemberReference)
{
MemberReference memberReference = reader.GetMemberReference((MemberReferenceHandle)handle);
typeName = this.GetMemberReferenceParentTypeName(reader, memberReference.Parent);
methodName = reader.GetString(memberReference.Name);
return !string.IsNullOrEmpty(typeName) && !string.IsNullOrEmpty(methodName);
}
if (handle.Kind == HandleKind.MethodDefinition)
{
MethodDefinition methodDefinition = reader.GetMethodDefinition((MethodDefinitionHandle)handle);
typeName = this.GetTypeDefinitionFullName(reader, methodDefinition.GetDeclaringType());
methodName = reader.GetString(methodDefinition.Name);
return !string.IsNullOrEmpty(typeName) && !string.IsNullOrEmpty(methodName);
}
if (handle.Kind == HandleKind.MethodSpecification)
{
MethodSpecification methodSpecification = reader.GetMethodSpecification((MethodSpecificationHandle)handle);
return this.TryGetMethodReference(methodSpecification.Method, reader, out typeName, out methodName);
}
return false;
}
private string TryGetTypeReference(EntityHandle handle, MetadataReader reader)
{
if (handle.Kind == HandleKind.TypeReference ||
handle.Kind == HandleKind.TypeDefinition ||
handle.Kind == HandleKind.TypeSpecification)
{
return this.GetEntityTypeName(reader, handle);
}
return string.Empty;
}
private void AddDecodedTypeViolations(
SecurityValidationResult result,
HashSet<string> seenViolations,
string decodedTypeName,
System.Func<string, string> createMessage,
System.Func<string, string> createDescription)
{
foreach (string typeName in ExpandDecodedTypeNames(decodedTypeName))
{
if (!DangerousApiCatalog.IsDangerousType(typeName))
{
continue;
}
this.AddViolation(
result,
seenViolations,
typeName,
createMessage(typeName),
createDescription(typeName));
}
}
private static IEnumerable<string> ExpandDecodedTypeNames(string decodedTypeName)
{
if (string.IsNullOrEmpty(decodedTypeName))
{
yield break;
}
string[] parts = decodedTypeName.Split(DecodedTypeNameSeparator);
foreach (string part in parts)
{
if (!string.IsNullOrEmpty(part))
{
yield return part;
}
}
}
private void AddViolation(
SecurityValidationResult result,
HashSet<string> seenViolations,
string apiName,
string message,
string description)
{
if (!seenViolations.Add($"{apiName}|{message}"))
{
return;
}
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = apiName,
Message = message,
Description = description,
Location = "il"
});
}
private string GetMemberReferenceParentTypeName(MetadataReader reader, EntityHandle parentHandle)
{
if (parentHandle.Kind == HandleKind.TypeReference ||
parentHandle.Kind == HandleKind.TypeDefinition ||
parentHandle.Kind == HandleKind.TypeSpecification)
{
return this.GetEntityTypeName(reader, parentHandle);
}
return string.Empty;
}
private string GetEntityTypeName(MetadataReader reader, EntityHandle handle)
{
if (handle.Kind == HandleKind.TypeReference)
{
return this.GetTypeReferenceFullName(reader, (TypeReferenceHandle)handle);
}
if (handle.Kind == HandleKind.TypeDefinition)
{
return this.GetTypeDefinitionFullName(reader, (TypeDefinitionHandle)handle);
}
if (handle.Kind == HandleKind.TypeSpecification)
{
return this.DecodeTypeSpecification(reader, (TypeSpecificationHandle)handle);
}
return string.Empty;
}
private string GetTypeReferenceFullName(MetadataReader reader, TypeReferenceHandle handle)
{
TypeReference typeReference = reader.GetTypeReference(handle);
string typeName = reader.GetString(typeReference.Name);
string namespaceName = reader.GetString(typeReference.Namespace);
if (typeReference.ResolutionScope.Kind == HandleKind.TypeReference)
{
string parentTypeName = this.GetTypeReferenceFullName(reader, (TypeReferenceHandle)typeReference.ResolutionScope);
return $"{parentTypeName}.{typeName}";
}
if (string.IsNullOrEmpty(namespaceName))
{
return typeName;
}
return $"{namespaceName}.{typeName}";
}
private string GetTypeDefinitionFullName(MetadataReader reader, TypeDefinitionHandle handle)
{
TypeDefinition typeDefinition = reader.GetTypeDefinition(handle);
string typeName = reader.GetString(typeDefinition.Name);
TypeDefinitionHandle declaringTypeHandle = typeDefinition.GetDeclaringType();
if (!declaringTypeHandle.IsNil)
{
string parentTypeName = this.GetTypeDefinitionFullName(reader, declaringTypeHandle);
return $"{parentTypeName}.{typeName}";
}
string namespaceName = reader.GetString(typeDefinition.Namespace);
if (string.IsNullOrEmpty(namespaceName))
{
return typeName;
}
return $"{namespaceName}.{typeName}";
}
private string DecodeTypeSpecification(MetadataReader reader, TypeSpecificationHandle handle)
{
TypeSpecification typeSpecification = reader.GetTypeSpecification(handle);
MetadataTypeNameDecoder decoder = new MetadataTypeNameDecoder(reader, this);
return typeSpecification.DecodeSignature(decoder, genericContext: null);
}
private static OpCode ReadOpCode(byte[] ilBytes, ref int offset)
{
byte first = ilBytes[offset++];
if (first != 0xFE)
{
return SingleByteOpCodes[first];
}
byte second = ilBytes[offset++];
return MultiByteOpCodes[second];
}
private static int ReadInt32(byte[] ilBytes, int offset)
{
return ilBytes[offset] |
(ilBytes[offset + 1] << 8) |
(ilBytes[offset + 2] << 16) |
(ilBytes[offset + 3] << 24);
}
private static int GetOperandSize(OperandType operandType, byte[] ilBytes, int offset)
{
switch (operandType)
{
case OperandType.InlineNone:
return 0;
case OperandType.ShortInlineBrTarget:
case OperandType.ShortInlineI:
case OperandType.ShortInlineVar:
return 1;
case OperandType.InlineVar:
return 2;
case OperandType.InlineI:
case OperandType.InlineBrTarget:
case OperandType.InlineField:
case OperandType.InlineMethod:
case OperandType.InlineSig:
case OperandType.InlineString:
case OperandType.InlineTok:
case OperandType.InlineType:
case OperandType.ShortInlineR:
return 4;
case OperandType.InlineI8:
case OperandType.InlineR:
return 8;
case OperandType.InlineSwitch:
int count = ReadInt32(ilBytes, offset);
return 4 + (count * 4);
default:
return 0;
}
}
private static OpCode[] BuildOpCodeTable(bool singleByte)
{
OpCode[] table = new OpCode[256];
System.Reflection.FieldInfo[] fields = typeof(OpCodes).GetFields();
foreach (System.Reflection.FieldInfo field in fields)
{
if (field.FieldType != typeof(OpCode))
{
continue;
}
OpCode opcode = (OpCode)field.GetValue(null);
ushort value = unchecked((ushort)opcode.Value);
if (singleByte)
{
if (value <= 0xFF)
{
table[value] = opcode;
}
continue;
}
if ((value & 0xFF00) == 0xFE00)
{
table[value & 0xFF] = opcode;
}
}
return table;
}
private sealed class MetadataTypeNameDecoder : ISignatureTypeProvider<string, object>
{
private readonly MetadataReader _reader;
private readonly PreloadIlSecurityValidator _validator;
public MetadataTypeNameDecoder(MetadataReader reader, PreloadIlSecurityValidator validator)
{
_reader = reader;
_validator = validator;
}
public string GetArrayType(string elementType, ArrayShape shape)
{
return elementType;
}
public string GetByReferenceType(string elementType)
{
return elementType;
}
public string GetFunctionPointerType(MethodSignature<string> signature)
{
return string.Empty;
}
public string GetGenericInstantiation(string genericType, ImmutableArray<string> typeArguments)
{
List<string> flattenedTypeNames = new List<string>();
AddFlattenedTypeName(flattenedTypeNames, genericType);
foreach (string typeArgument in typeArguments)
{
AddFlattenedTypeName(flattenedTypeNames, typeArgument);
}
return string.Join(DecodedTypeNameSeparator.ToString(), flattenedTypeNames);
}
public string GetGenericMethodParameter(object genericContext, int index)
{
return $"!!{index}";
}
public string GetGenericTypeParameter(object genericContext, int index)
{
return $"!{index}";
}
public string GetModifiedType(string modifierType, string unmodifiedType, bool isRequired)
{
return unmodifiedType;
}
public string GetPinnedType(string elementType)
{
return elementType;
}
public string GetPointerType(string elementType)
{
return elementType;
}
public string GetPrimitiveType(PrimitiveTypeCode typeCode)
{
return typeCode.ToString();
}
public string GetSZArrayType(string elementType)
{
return elementType;
}
public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
{
return _validator.GetTypeDefinitionFullName(reader, handle);
}
public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
{
return _validator.GetTypeReferenceFullName(reader, handle);
}
public string GetTypeFromSpecification(MetadataReader reader, object genericContext, TypeSpecificationHandle handle, byte rawTypeKind)
{
return _validator.DecodeTypeSpecification(reader, handle);
}
private static void AddFlattenedTypeName(List<string> flattenedTypeNames, string decodedTypeName)
{
if (string.IsNullOrEmpty(decodedTypeName))
{
return;
}
string[] parts = decodedTypeName.Split(DecodedTypeNameSeparator);
foreach (string part in parts)
{
if (!string.IsNullOrEmpty(part))
{
flattenedTypeNames.Add(part);
}
}
}
}
}
}