我将类的一个实例序列化为一个文件(BinaryFormatter
)
之后,在另一个项目中,我想反序列化这个文件,但它不起作用,因为我的新项目没有我的旧类的描述。 .Deserialize()
获得例外
Unable to find assembly '*MyAssembly, Version=1.9.0.0, Culture=neutral, PublicKeyToken=null'.*".
但是我有程序集的.DLL,其中包含我要反序列化的旧类的描述。
我不想在项目中添加这个DLL的引用(我希望能够反序列化任何类型的程序集的类......)
如何通知Serializer / Deserializer使用我动态加载的程序集?
答案 0 :(得分:4)
假设您正在通过Assembly.Load()
或Assembly.LoadFrom()
加载程序集,然后按照this answer到SerializationException for dynamically loaded Type到 Chris Shain 的说明进行操作,您可以使用AppDomain.AssemblyResolve
事件在反序列化期间加载动态程序集。但是,出于安全原因,您将要防止加载完全意外的程序集。
一种可能的实现方式是引入以下内容:
public class AssemblyResolver
{
readonly string assemblyFullPath;
readonly AssemblyName assemblyName;
public AssemblyResolver(string assemblyName, string assemblyFullPath)
{
// You might want to validate here that assemblyPath really is an absolute not relative path.
// See e.g. https://stackoverflow.com/questions/5565029/check-if-full-path-given
this.assemblyFullPath = assemblyFullPath;
this.assemblyName = new AssemblyName(assemblyName);
}
public ResolveEventHandler AssemblyResolve
{
get
{
return (o, a) =>
{
var name = new AssemblyName(a.Name);
if (name.Name == assemblyName.Name) // Check only the name if you want to ignore version. Otherwise you can just check string equality.
return Assembly.LoadFrom(assemblyFullPath);
return null;
};
}
}
}
然后,在启动时的某个地方,将适当的ResolveEventHandler
添加到AppDomain.CurrentDomain.AssemblyResolve
中,例如如下:
class Program
{
const string assemblyFullPath = @"C:\Full-path-to-my-assembly\MyAssembly.dll";
const string assemblyName = @"MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
static Program()
{
AppDomain.CurrentDomain.AssemblyResolve += new AssemblyResolver(assemblyName, assemblyFullPath).AssemblyResolve;
}
此ResolveEventHandler
检查以查看请求的程序集是否具有动态程序集的名称,如果有,则从预期的完整路径中加载当前版本。
一种替代方法是编写自定义SerializationBinder
并将其附加到BinaryFormatter.Binder
。在BindToType (string assemblyName, string typeName)
中,绑定程序需要检查属于动态程序集的类型,并适当地绑定到它们。这里的技巧是处理动态加载的类型嵌套在另一个程序集中的泛型中的情况,例如List<MyClass>
。在这种情况下,assemblyName
将是List<T>
而不是MyClass
的程序集的名称。有关如何执行此操作的详细信息,请参见
在comments @sgnsajgon中问,我想知道为什么不能像在项目中显式引用签名程序集时那样,对流进行反序列化吗?
尽管我不知道Microsoft员工在设计这些类时的想法(返回.Net 1.1),但这可能是因为:
用埃里克·利珀特(Eric Lippert)的话,no one ever designed, specified, implemented, tested, documented and shipped that feature.
formatter.Deserialize(stream)
的安全性已经有点像垃圾箱一样,但是在BinaryFormatter
流中对任何意外的程序集名称自动调用Assembly.Load()
可能会使情况变得更糟。
通过“垃圾箱大火”,我的意思是说,BinaryFormatter
将立即实例化并填充输入流中指定的类型,这些类型可能不是您期望的类型。因此,您可能会这样做
BinaryFormatter
但是,如果流实际上包含序列化的攻击小工具,例如TempFileCollection
,则将创建并填充该小工具,然后进行攻击。
(有关此类攻击的详细信息,请参见 TypeNameHandling caution in Newtonsoft Json , External json vulnerable because of Json.Net TypeNameHandling auto? , How to configure Json.NET to create a vulnerable web API 和AlvaroMuñoz&Oleksandr Mirosh的黑帽论文https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf。这些链接指定了如何修改Json.NET的配置以启用此类攻击; var instance = (MyClass)new BinaryFormatter().Deserialize(stream);
在默认配置下容易受到攻击。)
现在,如果BinaryFormatter
在无法识别的程序集名称上自动调用BinaryFormatter
,则使用它的应用可能会另外容易受到 DLL planting attack 的攻击,其中来自攻击DLL的攻击类型将从意外位置而不是安全位置意外加载,进一步加剧了攻击风险。
(顺便说一句,如果您确实选择编写自己的Assembly.Load()
,则可以过滤出意外类型和/或已知的攻击类型,从而降低了攻击小工具注入的风险。由于{ {1}}流通常包含您可能不知道不允许的序列化私有或内部类。)
顺便说一句, What are the deficiencies of the built-in BinaryFormatter based .Net serialization? 概述了使用SerializationBinder
可能遇到的其他问题。
答案 1 :(得分:2)
首先,有关二进制序列化的一些事实(如果仅对解决方案感兴趣,请跳过它们):
BinaryFormatter
。尽管仅在.NET 5中将其删除/标记为过时(不过,可以将其用作程序包),但.NET Core 2/3中也有很多类型,这些类型曾经在.NET Framework中可序列化。但无法在.NET Core中进行序列化(例如Type
,Encoding
,MemoryStream
,ResourceSet
,委托等)。如果您仍然确定要使用BinaryFormatter
解决问题,则可以使用以下选项:
1。最简单的情况:仅程序集版本已更改
您可以将简单的assemblyBinding
添加到app.config文件中。只需将实际版本放在newVersion
属性中即可。
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="MyAssembly" publicKeyToken="null" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
2。程序集名称和/或类型名称也已更改(或者,如果您更喜欢编程解决方案)
IFormatter
实现(因此也是BinaryFormatter
)具有Binder
属性。您可以使用它来控制程序集/类型名称解析:
internal class MyBinder : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
// mapping the known old type to the new one
if (assemblyName.StartsWith("MyAssembly, ") && typeName == "MyNamespace.MyOldType")
return typeof(MyNewType);
// for any other type returning null to apply the default resolving logic:
return null;
}
}
用法:
var formatter = new BinaryFormatter { Binder = new MyBinder() };
return (MyNewType)formatter.Deserialize(myStream);
如果您只需要汇编版本不敏感的解析器,则可以使用WeakAssemblySerializationBinder
。
3。新型的内部结构也发生了变化
由于OP并未涵盖这种情况,因此我不会深入探讨细节。 TL; DR:在这种情况下,您需要设置IFormatter.SurrogateSelector
属性。如果类型名称和内部布局都已更改,则可以将其与Binder
属性一起使用。如果您感兴趣,请在CustomSerializerSurrogateSelector
类的备注部分中找到一些子案例。
最终想法:
BinaryFormatter
可能不是实现目标的最佳选择。仅在确定要使用二进制序列化时,才使用上述解决方案。否则,您可以尝试使用XML或JSON序列化,该序列化基本上由公共成员序列化类型,并且不存储任何程序集信息。答案 2 :(得分:1)
二进制序列化对DLL Hell有着严肃的态度。它会在序列化数据时记录包含该类型的确切程序集。并且坚持在反序列化数据时找回确切的程序集。只有这样才能确保序列化数据与类型相匹配,采用任何快捷方式只会确保您在幸运垃圾数据时获得异常。这种情况迟早会发生的可能性是100%。
所以你需要完全废弃你可以使用“动态加载的程序集”并让它“反序列化任何类”的想法,这是一种幻觉。您可以旋转命运之轮并在app.exe.config文件中放置<bindingRedirect>
以强制CLR使用不同的程序集版本。处理事故现在是你的责任。许多程序员抓住机会,很少有人从经验中回来而没有学到新课程。必须要做的是了解后果。所以继续吧。