Skip to content

Commit cd8217a

Browse files
authored
Merge branch 'LogExperts:Development' into feature/dark-theme-color-tuning
2 parents 12c0630 + 2ae0070 commit cd8217a

4 files changed

Lines changed: 116 additions & 3 deletions

File tree

‎src/LogExpert.Core/Classes/JsonConverters/ColumnizerJsonConverter.cs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,11 @@ public override object ReadJson (JsonReader reader, Type objectType, object? exi
9696
columnizerType = Type.GetType(typeName, throwOnError: false);
9797
//FindColumnizerTypeByAssemblyQualifiedName(typeName);
9898

99-
// If Type.GetType fails, try finding it manually
100-
if (columnizerType == null)
99+
// If Type.GetType fails or finds something that is not a columnizer, try finding it manually
100+
if (columnizerType == null ||
101+
!typeof(ILogLineMemoryColumnizer).IsAssignableFrom(columnizerType) ||
102+
columnizerType.IsInterface ||
103+
columnizerType.IsAbstract)
101104
{
102105
columnizerType = FindColumnizerTypeByAssemblyQualifiedName(typeName);
103106
}

‎src/LogExpert.Tests/ColumnizerTests/ColumnizerJsonConverterTests.cs‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,39 @@ public void PushValue (ILogLineMemoryColumnizerCallback callback, int column, st
105105

106106
}
107107

108+
public sealed class NotAColumnizer
109+
{
110+
public static int ConstructorCalls { get; set; }
111+
112+
public NotAColumnizer ()
113+
{
114+
ConstructorCalls++;
115+
}
116+
}
117+
108118
[TestFixture]
109119
public class ColumnizerJsonConverterTests
110120
{
121+
[Test]
122+
public void Deserialize_TypeNameIsNotAColumnizer_ThrowsWithoutCreatingInstance ()
123+
{
124+
NotAColumnizer.ConstructorCalls = 0;
125+
var json = JsonConvert.SerializeObject(new
126+
{
127+
TypeName = typeof(NotAColumnizer).AssemblyQualifiedName,
128+
State = new { }
129+
});
130+
131+
var settings = new JsonSerializerSettings
132+
{
133+
TypeNameHandling = TypeNameHandling.None,
134+
Converters = { new ColumnizerJsonConverter() }
135+
};
136+
137+
_ = Assert.Throws<JsonSerializationException>(() => JsonConvert.DeserializeObject<ILogLineMemoryColumnizer>(json, settings));
138+
Assert.That(NotAColumnizer.ConstructorCalls, Is.Zero);
139+
}
140+
111141
[Test]
112142
public void SerializeDeserialize_MockColumnizer_RoundTripPreservesStateAndType ()
113143
{
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System.Reflection;
2+
using System.Reflection.Emit;
3+
4+
using NUnit.Framework;
5+
6+
namespace LogExpert.PluginRegistry.Tests;
7+
8+
/// <summary>
9+
/// The assembly resolver is reachable from type names stored in settings and .lxp files,
10+
/// so it must never load assemblies from outside the application and plugin directories.
11+
/// </summary>
12+
[TestFixture]
13+
[Category("PathTraversal")]
14+
[Category("Security")]
15+
public class AssemblyResolverTests
16+
{
17+
private string _probeDirectory = null!;
18+
private string _probeAssemblyName = null!;
19+
20+
[SetUp]
21+
public void SetUp ()
22+
{
23+
var id = Guid.NewGuid().ToString("N");
24+
_probeAssemblyName = "ResolveProbe_" + id;
25+
26+
// Sibling of the base directory, so it is reachable through "../" but outside it.
27+
_probeDirectory = Path.GetFullPath(Path.Join(AppDomain.CurrentDomain.BaseDirectory, "..", "resolve-probe-" + id));
28+
_ = Directory.CreateDirectory(_probeDirectory);
29+
30+
var assemblyBuilder = new PersistedAssemblyBuilder(new AssemblyName(_probeAssemblyName), typeof(object).Assembly);
31+
var typeBuilder = assemblyBuilder.DefineDynamicModule(_probeAssemblyName).DefineType("Probe", TypeAttributes.Public);
32+
_ = typeBuilder.CreateType();
33+
assemblyBuilder.Save(Path.Join(_probeDirectory, _probeAssemblyName + ".dll"));
34+
}
35+
36+
[TearDown]
37+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Unit Tests")]
38+
public void TearDown ()
39+
{
40+
try
41+
{
42+
Directory.Delete(_probeDirectory, recursive: true);
43+
}
44+
catch
45+
{
46+
// The DLL stays locked if a regression loaded it; the assertion already reported that.
47+
}
48+
}
49+
50+
[Test]
51+
public void ResolveHandler_AssemblyNameWithRelativePath_DoesNotLoadAssemblyOutsideBaseDirectory ()
52+
{
53+
var traversalName = $"../{Path.GetFileName(_probeDirectory)}/{_probeAssemblyName}";
54+
55+
// Call the handler directly: the test host installs its own resolvers that would mask the result.
56+
var handler = typeof(PluginRegistry).GetMethod("ColumnizerResolveEventHandler", BindingFlags.NonPublic | BindingFlags.Static);
57+
var resolved = handler!.Invoke(null, [null, new ResolveEventArgs(traversalName)]);
58+
59+
Assert.That(resolved, Is.Null);
60+
}
61+
62+
[Test]
63+
public void ResolveHandler_PlainAssemblyName_IsStillResolvedFromBaseDirectory ()
64+
{
65+
var handler = typeof(PluginRegistry).GetMethod("ColumnizerResolveEventHandler", BindingFlags.NonPublic | BindingFlags.Static);
66+
var resolved = handler!.Invoke(null, [null, new ResolveEventArgs("LogExpert.PluginRegistry")]) as Assembly;
67+
68+
Assert.That(resolved?.GetName().Name, Is.EqualTo("LogExpert.PluginRegistry"));
69+
}
70+
}

‎src/PluginRegistry/PluginRegistry.cs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1277,7 +1277,17 @@ private static T TryInstantiate<T> (Type loadedType, IFileSystemCallback fsCallb
12771277

12781278
private static Assembly ColumnizerResolveEventHandler (object? sender, ResolveEventArgs args)
12791279
{
1280-
var fileName = new AssemblyName(args.Name).Name + ".dll";
1280+
var assemblyName = new AssemblyName(args.Name).Name;
1281+
1282+
// Type names come from settings and .lxp files; a name like "../x" must not reach outside our directories.
1283+
if (string.IsNullOrEmpty(assemblyName) ||
1284+
assemblyName != Path.GetFileName(assemblyName) ||
1285+
assemblyName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
1286+
{
1287+
return null;
1288+
}
1289+
1290+
var fileName = assemblyName + ".dll";
12811291

12821292
var mainDir = Path.Join(AppDomain.CurrentDomain.BaseDirectory, fileName);
12831293
var pluginDir = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "plugins", fileName);

0 commit comments

Comments
 (0)