chore(unity): 固定Unity CLI Loop本地包并移除UnitySkills依赖
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user