feat(security): 仅加载显式授权的外部 DLL
Publish UPM package / publish (push) Successful in 2s

This commit is contained in:
2026-08-28 03:44:10 +08:00
parent 39f6f97707
commit f6688ce573
13 changed files with 352 additions and 74 deletions
+76 -3
View File
@@ -286,7 +286,23 @@ namespace ShrinkModFramework.Tests
}
[Test]
public void ExternalDllRevision_FailedReplacementRestoresPreviousAndBrokenBytesDoNotReplaceIt()
public void Settings_Defaults_DoNotAutoLoadOrWatchExternalDlls()
{
var settings = ScriptableObject.CreateInstance<ShrinkModFrameworkSettings>();
try
{
Assert.IsFalse(settings.autoLoadOnStartup);
Assert.IsFalse(settings.enableExternalDllMods);
Assert.IsFalse(settings.watchExternalModsDirectory);
}
finally
{
Object.DestroyImmediate(settings);
}
}
[Test]
public void ExternalDll_IsIgnoredUntilItsExactPathIsAuthorized()
{
ResetExternalRuntime();
var settings = CreateExternalSettings();
@@ -299,6 +315,62 @@ namespace ShrinkModFramework.Tests
CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target);
ShrinkModLoader.LoadAll(settings);
Assert.IsEmpty(ShrinkModLoader.Mods);
Assert.AreEqual(0,
ShrinkModDiagnostics.CaptureExternalAssemblies(settings).CurrentRevisionCount);
ShrinkModLoader.LoadAuthorized(settings, new[] { target });
AssertExternalFixture("1.0.0", "v1");
}
finally
{
CleanupExternalRuntime(settings, directory);
}
}
[Test]
public void LegacyLoader_OnlyLoadsExplicitlyAuthorizedDllPaths()
{
ResetExternalRuntime();
var settings = CreateExternalSettings();
settings.useContextHost = false;
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings);
var authorized = Path.Combine(directory, "Authorized.Mod.dll");
var unauthorized = Path.Combine(directory, "Unauthorized.Mod.dll");
try
{
Directory.CreateDirectory(directory);
CopyFixture("ExternalFixture.Mod.V1.dll.bytes", authorized);
CopyFixture("ExternalFixture.Mod.V3.dll.bytes", unauthorized);
ShrinkModLoader.LoadAuthorized(settings, new[] { authorized });
AssertExternalFixture("1.0.0", "v1");
Assert.AreEqual(1,
ShrinkModDiagnostics.CaptureExternalAssemblies(settings).CurrentRevisionCount);
}
finally
{
CleanupExternalRuntime(settings, directory);
}
}
[Test]
public void ExternalDllRevision_FailedReplacementRestoresPreviousAndBrokenBytesDoNotReplaceIt()
{
ResetExternalRuntime();
var settings = CreateExternalSettings();
var directory = ShrinkExternalModAssemblyLoader.GetExternalModsDirectory(settings);
var target = Path.Combine(directory, "ExternalFixture.Mod.dll");
try
{
Directory.CreateDirectory(directory);
CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target);
ShrinkModLoader.LoadAuthorized(settings, new[] { target });
AssertExternalFixture("1.0.0", "v1");
var restoredGeneration = ShrinkModLoader.Mods["external.fixture"].Generation;
@@ -358,7 +430,7 @@ namespace ShrinkModFramework.Tests
Assert.IsTrue(ShrinkModRuntimeDriver.InstanceForTesting!.HasWatcherForTesting);
CopyFixture("ExternalFixture.Mod.V1.dll.bytes", target);
ShrinkModLoader.LoadAll(settings);
ShrinkModLoader.LoadAuthorized(settings, new[] { target });
AssertExternalFixture("1.0.0", "v1");
// Two writes arrive as one debounce window; only the final valid revision should commit.
@@ -395,7 +467,7 @@ namespace ShrinkModFramework.Tests
CopyFixture("ExternalFixture.Provider.dll.bytes", providerPath);
CopyFixture("ExternalFixture.Consumer.dll.bytes", consumerPath);
ShrinkModLoader.LoadAll(settings);
ShrinkModLoader.LoadAuthorized(settings, new[] { providerPath, consumerPath });
Assert.AreEqual(2, ShrinkModLoader.Mods.Count);
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.provider"].State);
Assert.AreEqual(ShrinkModState.Ready, ShrinkModLoader.Mods["external.consumer"].State);
@@ -475,6 +547,7 @@ namespace ShrinkModFramework.Tests
settings.useContextHost = true;
settings.enableExternalDllMods = true;
settings.autoCreateExternalModsDirectory = false;
settings.watchExternalModsDirectory = true;
settings.externalModsFolderName = "ShrinkModFrameworkTests_" + Guid.NewGuid().ToString("N");
settings.assemblyNamePrefixes = new[] { "ExternalFixture." };
settings.enableHarmonyPatching = false;