Files
ft/Client/Packages/io.github.hatayama.uloopmcp/Editor/Compilation/IlSecurityValidator.cs

383 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Post-compilation security validator using IL byte inspection.
/// Scans call/callvirt/newobj/ldtoken opcodes to detect dangerous API usage
/// that source-level scanning cannot catch (e.g. using aliases, fully-qualified names).
/// </summary>
internal sealed class IlSecurityValidator
{
private static readonly OpCode[] SingleByteOpCodes = BuildOpCodeTable(singleByte: true);
private static readonly OpCode[] MultiByteOpCodes = BuildOpCodeTable(singleByte: false);
public SecurityValidationResult Validate(Assembly assembly)
{
SecurityValidationResult result = new SecurityValidationResult
{
IsValid = true,
Violations = new List<SecurityViolation>()
};
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = Array.FindAll(ex.Types, static t => t != null);
}
foreach (Type type in types)
{
ValidateTypeHierarchy(type, result);
ValidateMethodReferences(type, result);
}
result.IsValid = result.Violations.Count == 0;
return result;
}
private static void ValidateTypeHierarchy(Type type, SecurityValidationResult result)
{
ValidateReferencedType(
type.BaseType,
result,
typeName => $"Inheriting from dangerous type: {typeName}",
typeName => $"Type '{type.FullName}' inherits from blocked type '{typeName}'");
foreach (Type iface in type.GetInterfaces())
{
ValidateReferencedType(
iface,
result,
typeName => $"Implementing dangerous interface: {typeName}",
typeName => $"Type '{type.FullName}' implements blocked interface '{typeName}'");
}
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.DeclaredOnly;
foreach (FieldInfo field in type.GetFields(flags))
{
ValidateReferencedType(
field.FieldType,
result,
typeName => $"Using dangerous type as field: {typeName}",
typeName => $"Field '{field.Name}' of type '{typeName}' in {type.FullName}");
}
foreach (PropertyInfo property in type.GetProperties(flags))
{
ValidateReferencedType(
property.PropertyType,
result,
typeName => $"Using dangerous type as property: {typeName}",
typeName => $"Property '{property.Name}' of type '{typeName}' in {type.FullName}");
foreach (System.Reflection.ParameterInfo indexParameter in property.GetIndexParameters())
{
ValidateReferencedType(
indexParameter.ParameterType,
result,
typeName => $"Using dangerous type as property index parameter: {typeName}",
typeName => $"Property index parameter '{indexParameter.Name}' of type '{typeName}' in {type.FullName}.{property.Name}");
}
}
foreach (EventInfo eventInfo in type.GetEvents(flags))
{
ValidateReferencedType(
eventInfo.EventHandlerType,
result,
typeName => $"Using dangerous type as event handler: {typeName}",
typeName => $"Event '{eventInfo.Name}' of type '{typeName}' in {type.FullName}");
}
}
private static void ValidateMethodReferences(Type type, SecurityValidationResult result)
{
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.DeclaredOnly;
foreach (MethodInfo method in type.GetMethods(flags))
{
ValidateMethodBody(method, result);
}
foreach (ConstructorInfo ctor in type.GetConstructors(flags))
{
ValidateMethodBody(ctor, result);
}
}
private static void ValidateMethodBody(MethodBase method, SecurityValidationResult result)
{
MethodBody body;
try
{
body = method.GetMethodBody();
}
catch (InvalidOperationException)
{
// Abstract/extern methods have no body
return;
}
if (body == null) return;
foreach (LocalVariableInfo local in body.LocalVariables)
{
string typeName = local.LocalType?.FullName;
if (typeName != null && DangerousApiCatalog.IsDangerousType(typeName))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = typeName,
Message = $"Using dangerous type as local variable: {typeName}",
Description = $"Local variable of type '{typeName}' in {method.DeclaringType?.FullName}.{method.Name}"
});
}
}
foreach (System.Reflection.ParameterInfo param in method.GetParameters())
{
string typeName = param.ParameterType?.FullName;
if (typeName != null && DangerousApiCatalog.IsDangerousType(typeName))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = typeName,
Message = $"Using dangerous type as parameter: {typeName}",
Description = $"Parameter '{param.Name}' of type '{typeName}' in {method.DeclaringType?.FullName}.{method.Name}"
});
}
}
byte[] il = body.GetILAsByteArray();
if (il == null) return;
ValidateIlCalls(method, il, result);
}
private static void ValidateReferencedType(
Type referencedType,
SecurityValidationResult result,
Func<string, string> createMessage,
Func<string, string> createDescription)
{
foreach (string typeName in ExpandReferencedTypes(referencedType))
{
if (!DangerousApiCatalog.IsDangerousType(typeName))
{
continue;
}
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = typeName,
Message = createMessage(typeName),
Description = createDescription(typeName)
});
}
}
private static IEnumerable<string> ExpandReferencedTypes(Type referencedType)
{
if (referencedType == null)
{
yield break;
}
if (referencedType.HasElementType)
{
foreach (string typeName in ExpandReferencedTypes(referencedType.GetElementType()))
{
yield return typeName;
}
yield break;
}
string fullName = referencedType.FullName;
if (!string.IsNullOrEmpty(fullName))
{
yield return fullName;
}
if (!referencedType.IsGenericType)
{
yield break;
}
foreach (Type genericArgument in referencedType.GetGenericArguments())
{
foreach (string typeName in ExpandReferencedTypes(genericArgument))
{
yield return typeName;
}
}
}
private static void ValidateIlCalls(MethodBase method, byte[] il, SecurityValidationResult result)
{
int offset = 0;
Module module = method.Module;
Type[] typeArgs = method.DeclaringType?.IsGenericType == true
? method.DeclaringType.GetGenericArguments()
: null;
Type[] methodArgs = method.IsGenericMethod ? method.GetGenericArguments() : null;
while (offset < il.Length)
{
OpCode opCode = ReadOpCode(il, ref offset);
if (opCode.OperandType == OperandType.InlineMethod)
{
int token = ReadInt32(il, offset);
if (opCode == OpCodes.Call || opCode == OpCodes.Callvirt || opCode == OpCodes.Newobj ||
opCode == OpCodes.Ldftn || opCode == OpCodes.Ldvirtftn)
{
MethodBase calledMethod;
try
{
calledMethod = module.ResolveMethod(token, typeArgs, methodArgs);
}
catch (ArgumentOutOfRangeException)
{
offset += 4;
continue;
}
catch (ArgumentException)
{
offset += 4;
continue;
}
string declaringType = calledMethod.DeclaringType?.FullName;
string memberName = calledMethod.Name;
if (declaringType != null && DangerousApiCatalog.IsDangerousApi(declaringType, memberName))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = $"{declaringType}.{memberName}",
Message = $"Dangerous API call: {declaringType}.{memberName}",
Description = $"IL {opCode.Name} to '{declaringType}.{memberName}' in {method.DeclaringType?.FullName}.{method.Name}"
});
}
}
}
// ldtoken with InlineTok operand: detect typeof(DangerousType)
if (opCode == OpCodes.Ldtoken && opCode.OperandType == OperandType.InlineTok)
{
int token = ReadInt32(il, offset);
Type resolvedType;
try
{
resolvedType = module.ResolveType(token, typeArgs, methodArgs);
}
catch (ArgumentOutOfRangeException)
{
// ldtoken can also reference fields/methods, not just types
offset += GetOperandSize(opCode.OperandType, il, offset);
continue;
}
catch (ArgumentException)
{
// ldtoken can also reference fields/methods, not just types
offset += GetOperandSize(opCode.OperandType, il, offset);
continue;
}
string fullName = resolvedType.FullName;
if (fullName != null && DangerousApiCatalog.IsDangerousType(fullName))
{
result.Violations.Add(new SecurityViolation
{
Type = SecurityViolationType.DangerousApiCall,
ApiName = fullName,
Message = $"typeof() reference to dangerous type: {fullName}",
Description = $"typeof({fullName}) in {method.DeclaringType?.FullName}.{method.Name}"
});
}
}
offset += GetOperandSize(opCode.OperandType, il, offset);
}
}
private static OpCode ReadOpCode(byte[] il, ref int offset)
{
byte first = il[offset++];
if (first != 0xFE) return SingleByteOpCodes[first];
byte second = il[offset++];
return MultiByteOpCodes[second];
}
private static int ReadInt32(byte[] il, int offset)
{
return il[offset] | (il[offset + 1] << 8) | (il[offset + 2] << 16) | (il[offset + 3] << 24);
}
private static int GetOperandSize(OperandType operandType, byte[] il, 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(il, offset);
return 4 + (count * 4);
default: return 0;
}
}
private static OpCode[] BuildOpCodeTable(bool singleByte)
{
OpCode[] table = new OpCode[0x100];
foreach (FieldInfo field in typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (field.GetValue(null) is not OpCode opCode) continue;
ushort value = unchecked((ushort)opCode.Value);
if (singleByte && value <= 0xFF)
{
table[value] = opCode;
}
else if (!singleByte && (value & 0xFF00) == 0xFE00)
{
table[value & 0xFF] = opCode;
}
}
return table;
}
}
}