FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Add IClrTypeFilter to restrict which CLR types reach Python · pythonnet/pythonnet@e08a730 · GitHub

Repository navigation

Commit e08a730

Browse files
committed
Add IClrTypeFilter to restrict which CLR types reach Python
Embedders can register filters on InteropConfiguration.ClrTypeFilters to refuse CLR types before they're handed to Python. Empty by default - a type is allowed only if every registered filter permits it ReflectedClrType.GetOrCreate consults the filters on a cache miss, before taking _cacheCreateLock, so filter code never runs while a pythonnet lock is held. Every CLR type passes through there, so the one check covers return values, fields, iterators and APIs returning System.Type alike. Refused types are checked everytime, but a refusal throws ClrTypeFilteredException, which Exceptions.SetError raises as a plain Python TypeError: wrapping the CLR exception would mean reflecting its type into Python, which the same filters may refuse. InterfaceObject now guards __implementation__ and __raw_implementation__, which expose the concrete object and so can be refused too NOTE: this isn't a security feature, but it does make it harder to go exploring through everything available in the runtime
1 parent 2fe63b5 commit e08a730

8 files changed

Lines changed: 268 additions & 4 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
using NUnit.Framework;
5+
6+
using Python.Runtime;
7+
8+
namespace Python.EmbeddingTest
9+
{
10+
public class TestClrTypeFilter
11+
{
12+
static readonly string RefusedName = typeof(FilterRefusedType).FullName!;
13+
14+
readonly RefuseOneType _filter = new(typeof(FilterRefusedType));
15+
16+
// The engine is already running, so the filter is added to the live
17+
// configuration, as Inheritance does with PythonBaseTypeProviders. That
18+
// is safe here because nothing else ever reflects FilterRefusedType, so
19+
// it cannot already be cached
20+
[OneTimeSetUp]
21+
public void SetUp() => PythonEngine.InteropConfiguration.ClrTypeFilters.Add(_filter);
22+
23+
[OneTimeTearDown]
24+
public void TearDown() => PythonEngine.InteropConfiguration.ClrTypeFilters.Remove(_filter);
25+
26+
[TestCase("host.Method()", TestName = "MethodReturn")]
27+
[TestCase("host.OutParam(None)", TestName = "OutParameterAndReturn")]
28+
[TestCase("host.OutOnly(None)", TestName = "OutParameterOnly")]
29+
[TestCase("list(host.Iterate())", TestName = "IteratorItem")]
30+
[TestCase("host.Property", TestName = "InstanceProperty")]
31+
[TestCase("type(host).StaticProperty", TestName = "StaticProperty")]
32+
[TestCase("host.Field", TestName = "InstanceField")]
33+
[TestCase("type(host).StaticField", TestName = "StaticField")]
34+
[TestCase("host.Array1[0]", TestName = "ArrayItem")]
35+
[TestCase("host.Array2[0, 0]", TestName = "MultidimensionalArrayItem")]
36+
[TestCase("host[0]", TestName = "IndexerResult")]
37+
[TestCase("host.AsInterface().__implementation__", TestName = "InterfaceImplementation")]
38+
[TestCase("host.AsInterface().__raw_implementation__", TestName = "InterfaceRawImplementation")]
39+
public void RefusedTypeRaisesCatchableTypeError(string expression)
40+
{
41+
using var scope = Py.CreateScope();
42+
scope.Set("host", new FilterHost());
43+
scope.Exec($@"
44+
def attempt():
45+
try:
46+
{expression}
47+
except TypeError as e:
48+
return str(e)
49+
return 'nothing raised'
50+
");
51+
Assert.That(scope.Eval<string>("attempt()"), Does.Contain(RefusedName));
52+
}
53+
54+
[Test]
55+
public void PermittedTypesAreUnaffected()
56+
{
57+
using var scope = Py.CreateScope();
58+
scope.Set("host", new FilterHost());
59+
Assert.That(scope.Eval<int>("host.Permitted().Value"), Is.EqualTo(42));
60+
}
61+
62+
[Test]
63+
public void UncaughtRefusalReachesTheCallerAsTypeError()
64+
{
65+
using var scope = Py.CreateScope();
66+
scope.Set("host", new FilterHost());
67+
68+
var error = Assert.Throws<PythonException>(() => scope.Exec("host.Method()"));
69+
Assert.That(error.Type.Name, Is.EqualTo("TypeError"));
70+
Assert.That(error.Message, Does.Contain(RefusedName));
71+
}
72+
73+
[Test]
74+
public void RefusedTypeIsCheckedOnEveryAttempt()
75+
{
76+
var counter = new CountingFilter(typeof(FilterRefusedType));
77+
var filters = PythonEngine.InteropConfiguration.ClrTypeFilters;
78+
filters.Insert(0, counter);
79+
try
80+
{
81+
using var scope = Py.CreateScope();
82+
scope.Set("host", new FilterHost());
83+
for (var i = 0; i < 2; i++)
84+
Assert.Throws<PythonException>(() => scope.Exec("host.Method()"));
85+
86+
// A refusal is never cached, so the filters see the type again
87+
Assert.That(counter.Count, Is.EqualTo(2));
88+
}
89+
finally
90+
{
91+
filters.Remove(counter);
92+
}
93+
}
94+
95+
[Test]
96+
public void NoFiltersByDefault()
97+
{
98+
using var configuration = InteropConfiguration.MakeDefault();
99+
Assert.That(configuration.ClrTypeFilters, Is.Empty);
100+
}
101+
102+
[Test]
103+
public void TypeIsReflectedOnlyIfEveryFilterPermitsIt()
104+
{
105+
var group = new ClrTypeFilterGroup();
106+
Assert.That(group.ShouldReflect(typeof(object)), Is.True, "an empty group permits everything");
107+
108+
group.Add(new RefuseOneType(typeof(string)));
109+
Assert.That(group.ShouldReflect(typeof(object)), Is.True);
110+
Assert.That(group.ShouldReflect(typeof(string)), Is.False);
111+
112+
group.Add(new RefuseOneType(typeof(object)));
113+
Assert.That(group.ShouldReflect(typeof(object)), Is.False);
114+
}
115+
116+
sealed class RefuseOneType(Type refused) : IClrTypeFilter
117+
{
118+
public bool ShouldReflect(Type type) => type != refused;
119+
}
120+
121+
sealed class CountingFilter(Type counted) : IClrTypeFilter
122+
{
123+
public int Count { get; private set; }
124+
125+
public bool ShouldReflect(Type type)
126+
{
127+
if (type == counted)
128+
Count++;
129+
return true;
130+
}
131+
}
132+
}
133+
134+
/// <summary>Refused by <see cref="TestClrTypeFilter"/>; reflected nowhere else.</summary>
135+
public class FilterRefusedType : IFilterTarget { }
136+
137+
public interface IFilterTarget { }
138+
139+
public class FilterPermittedType
140+
{
141+
public int Value => 42;
142+
}
143+
144+
/// <summary>Hands a refused type to Python through each boundary.</summary>
145+
public class FilterHost
146+
{
147+
public FilterRefusedType Method() => new();
148+
public int OutParam(out FilterRefusedType value) { value = new(); return 1; }
149+
public void OutOnly(out FilterRefusedType value) => value = new();
150+
public IEnumerable<FilterRefusedType> Iterate() { yield return new(); }
151+
public FilterRefusedType Property => new();
152+
public static FilterRefusedType StaticProperty => new();
153+
public FilterRefusedType Field = new();
154+
public static FilterRefusedType StaticField = new();
155+
public FilterRefusedType[] Array1 = { new() };
156+
public FilterRefusedType[,] Array2 = { { new() } };
157+
public FilterRefusedType this[int index] => new();
158+
public IFilterTarget AsInterface() => new FilterRefusedType();
159+
public FilterPermittedType Permitted() => new();
160+
}
161+
}

‎src/runtime/ClrTypeFilterGroup.cs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Python.Runtime
6+
{
7+
/// <summary>
8+
/// Composes the registered filters: a type is reflected only if every filter
9+
/// permits it. An empty group permits everything, which is the default.
10+
/// </summary>
11+
class ClrTypeFilterGroup : List<IClrTypeFilter>, IClrTypeFilter
12+
{
13+
public bool ShouldReflect(Type type)
14+
{
15+
if (type is null)
16+
throw new ArgumentNullException(nameof(type));
17+
18+
foreach (var filter in this)
19+
if (!filter.ShouldReflect(type))
20+
return false;
21+
22+
return true;
23+
}
24+
}
25+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
namespace Python.Runtime
4+
{
5+
/// <summary>
6+
/// Raised when a CLR type would be reflected into Python but an
7+
/// <see cref="IClrTypeFilter"/> registered on
8+
/// <see cref="InteropConfiguration.ClrTypeFilters"/> refused it.
9+
/// </summary>
10+
public class ClrTypeFilteredException : Exception
11+
{
12+
public Type FilteredType { get; }
13+
14+
public ClrTypeFilteredException(Type type)
15+
: base($"Reflecting {type?.FullName ?? "<null>"} into Python was refused by an IClrTypeFilter.")
16+
=> FilteredType = type!;
17+
}
18+
}

‎src/runtime/Exceptions.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ public static bool SetError(Exception e)
202202
return true;
203203
}
204204

205+
// Raised as a plain TypeError: wrapping the CLR exception would mean
206+
// reflecting its type into Python, which the same filters may refuse
207+
if (e is ClrTypeFilteredException)
208+
{
209+
SetError(TypeError, e.Message);
210+
return true;
211+
}
212+
205213
using var instance = Converter.ToPython(e);
206214
if (instance.IsNull()) return false;
207215

‎src/runtime/IClrTypeFilter.cs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
3+
namespace Python.Runtime
4+
{
5+
/// <summary>
6+
/// Decides whether a CLR type may be reflected into Python.
7+
/// <para>
8+
/// Filters are consulted when a type would first be presented to Python.
9+
/// Permitted types are cached for the lifetime of the engine and not
10+
/// re-checked; a refused type is checked again on each attempt
11+
/// </para>
12+
/// <para>
13+
/// This is a policy hook for embedders, not a security boundary: Python code
14+
/// holding any reflected object can still reach whatever that object's own
15+
/// API exposes.
16+
/// </para>
17+
/// </summary>
18+
public interface IClrTypeFilter
19+
{
20+
/// <summary>
21+
/// Whether <paramref name="type"/> may be reflected into Python. Returning
22+
/// false causes an attempt to present the type to Python to fail.
23+
/// </summary>
24+
bool ShouldReflect(Type type);
25+
}
26+
}

‎src/runtime/InteropConfiguration.cs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ internal readonly PythonBaseTypeProviderGroup pythonBaseTypeProviders
1414
/// <summary>Enables replacing base types of CLR types as seen from Python</summary>
1515
public IList<IPythonBaseTypeProvider> PythonBaseTypeProviders => this.pythonBaseTypeProviders;
1616

17+
internal readonly ClrTypeFilterGroup clrTypeFilters = new();
18+
19+
/// <summary>
20+
/// Restricts which CLR types are reflected into Python. Empty by default,
21+
/// so every type is reflected; a type is presented to Python only if every
22+
/// registered filter permits it.
23+
/// </summary>
24+
public IList<IClrTypeFilter> ClrTypeFilters => this.clrTypeFilters;
25+
1726
public static InteropConfiguration MakeDefault()
1827
{
1928
return new InteropConfiguration

‎src/runtime/Types/InterfaceObject.cs‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,24 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k
106106
}
107107

108108
string? name = Runtime.GetManagedString(key);
109-
if (name == "__implementation__")
109+
110+
// Both expose the concrete object, whose type an IClrTypeFilter may
111+
// refuse - that must become a Python exception, not unwind through it
112+
try
110113
{
111-
return Converter.ToPython(clrObj.inst);
114+
if (name == "__implementation__")
115+
{
116+
return Converter.ToPython(clrObj.inst);
117+
}
118+
else if (name == "__raw_implementation__")
119+
{
120+
return CLRObject.GetReference(clrObj.inst);
121+
}
112122
}
113-
else if (name == "__raw_implementation__")
123+
catch (Exception e)
114124
{
115-
return CLRObject.GetReference(clrObj.inst);
125+
Exceptions.SetError(e);
126+
return default;
116127
}
117128

118129
return Runtime.PyObject_GenericGetAttr(ob, key);

‎src/runtime/Types/ReflectedClrType.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ public static ReflectedClrType GetOrCreate(Type type)
2929
if (ClassManager.cache.TryGetValue(type, out var pyType))
3030
return pyType;
3131

32+
// Every CLR type reaching Python passes through here, so consulting the
33+
// filters once, on the cache miss, covers every route a type can take:
34+
// GetType(), an API returning System.Type, a field, a return value
35+
if (!PythonEngine.InteropConfiguration.clrTypeFilters.ShouldReflect(type))
36+
throw new ClrTypeFilteredException(type);
37+
3238
// Shared with ClassManager.cache + TypeManager._slotsHolders writes
3339
// so the multi-step type build below is atomic.
3440
lock (ClassManager._cacheCreateLock)

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL