Skip to content

Harden CodeOps against reflection-based whitelist bypass - #3643

Open
eanz17 wants to merge 8 commits into
AElfProject:devfrom
eanz17:security/codeops-reflection-guard
Open

Harden CodeOps against reflection-based whitelist bypass#3643
eanz17 wants to merge 8 commits into
AElfProject:devfrom
eanz17:security/codeops-reflection-guard

Conversation

@eanz17

@eanz17 eanz17 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Hardens the C# contract auditor (AElf.CSharp.CodeOps) against a reflection-based whitelist bypass and closes the adjacent static-analysis blind spots that made it possible.

The whitelist validator is a static IL member-reference scan. Because System.Type was whitelisted with all members allowed, contract code could use reflection — Type.GetType(string) + Type.InvokeMember(...) — to dispatch calls dynamically. String-based reflection leaves no static type reference for the whitelist to catch, so a contract could reach code paths (e.g. dynamic assembly loading) that let a second assembly execute at runtime without ever being submitted to the auditor — a sandbox-escape primitive.

Changes

1. Whitelist: System.Type is no longer fully allowed (IWhitelistProvider.cs)
Only GetTypeFromHandle (what typeof(x) lowers to; used by protobuf-generated descriptor code) and op_Equality/op_Inequality (so typeof(a) == typeof(b) still works) are permitted. GetType(string) / InvokeMember / GetMethod(s) / GetField(s) / GetProperty(ies) / MakeGenericType / … are denied.

2. New ReflectionValidator (Validators/Method/ReflectionValidator.cs)
IValidator<MethodDefinition>, auto-registered via ITransientDependency. Denies the dynamic-dispatch / dynamic code-loading & generation surface by (declaring type, method): System.Type reflection methods, Activator, Assembly, MethodBase/MethodInfo/ConstructorInfo/FieldInfo/PropertyInfo, AssemblyLoadContext, Reflection.Emit.*, Marshal, Delegate.DynamicInvoke, System.Linq.Expressions.*.Compile. Still allowed: typeof(x), obj.GetType() (→ Object.GetType), RuntimeHelpers.InitializeArray.

3. Whitelist now validates parameter, generic-argument and local-variable types (IWhitelistValidator.cs)
Previously only declaring type, method name and return type were checked, so a denied type used only as a parameter (e.g. BindingFlags on Type.InvokeMember) or a local slipped through. ValidateReference short-circuits for fully-trusted and generic-parameter types, so this only tightens the untrusted surface.

4. Compatibility additions (required by #3)
Whitelist System.RuntimeTypeHandle / RuntimeFieldHandle / RuntimeMethodHandle as bare type references — typeof(x) lowers to GetTypeFromHandle(RuntimeTypeHandle) and hardcoded array init to RuntimeHelpers.InitializeArray(Array, RuntimeFieldHandle); without this, parameter validation would reject both in every contract.

5. System.Linq.Expressions downgraded from Trust.Full to Trust.Partial
Expression.Compile() is a runtime code generator. LINQ-to-objects (System.Linq) is unaffected.

Compatibility

  • System-contract source (the ContractAuditorTests corpus: Token, Genesis, AEDPoS, Election, Parliament, Association, …) uses no reflection dispatch and no hand-written typeof; generated protobuf reflection uses System.Type only via typeof(...).
  • obj.GetType() keeps working (declaring type is System.Object, not System.Type).
  • The changes were exercised against Mono.Cecil over compiled snippets: typeof(a)==typeof(b), hardcoded array init, checked arithmetic, LINQ generic methods and string interpolation all still pass; reflection dispatch, BindingFlags/Assembly parameter/local types are rejected.

Testing

  • ReflectionValidatorTests and WhitelistReflectionHardeningTests (AElf.CSharp.CodeOps.UnitTests).
  • Reviewers: please run the full CodeOps corpus (AElf.CSharp.CodeOps.Tests + ...UnitTests) to confirm every existing system/user contract still passes audit — that regression is the gate for the parameter/local-type validation.

Notes

This PR scopes to the static-analysis layers. Further runtime-containment hardening is tracked separately.

🤖 Generated with Claude Code

jason-aelf and others added 5 commits March 11, 2026 16:53
The contract auditor's whitelist is a static IL member-reference scan. Because
System.Type was whitelisted with all members allowed, contract code could use
reflection (Type.GetType(string) + Type.InvokeMember(...)) to dispatch calls
dynamically, reaching code paths that load a second assembly at runtime which is
never submitted to the auditor. String-based reflection leaves no static type
reference for the whitelist to catch, so this evaded static validation.

Add two independent layers of defense:

- Whitelist: System.Type is no longer fully allowed. Only Type.GetTypeFromHandle
  is permitted (what typeof(x) lowers to; used by protobuf-generated descriptor
  code). Type.GetType(string) / InvokeMember / GetMethod(s) / GetField(s) /
  GetProperty(ies) / MakeGenericType / ... are now denied. Bare Type references
  and typeof(x) still pass.

- New ReflectionValidator (IValidator<MethodDefinition>): denies the dynamic
  dispatch and dynamic code-loading/generation surface by (declaring type,
  method): System.Type reflection methods, Activator, Assembly,
  MethodBase/MethodInfo/ConstructorInfo/FieldInfo/PropertyInfo, AssemblyLoadContext,
  Reflection.Emit.*, Marshal, Delegate.DynamicInvoke and Expression.Compile.
  typeof(x), obj.GetType() (Object.GetType) and RuntimeHelpers.InitializeArray
  remain allowed. Auto-registered via ITransientDependency like the other method
  validators.

Add ReflectionValidatorTests covering both the rejected reflection patterns and
the allowed typeof() / obj.GetType() cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…list

Follow-up hardening layered on the reflection guard:

- The whitelist now validates method parameter types and generic arguments
  (previously only declaring type, name and return type were checked) and method
  local-variable types. This closes the blind spots where a denied type is used
  only as a parameter (e.g. BindingFlags on Type.InvokeMember) or as a local.

- System.Type: additionally allow op_Equality / op_Inequality so
  typeof(a) == typeof(b) keeps working under the tightened Type rule.

- Whitelist System.RuntimeTypeHandle / RuntimeFieldHandle / RuntimeMethodHandle
  as bare type references. Required now that parameter/local types are validated:
  typeof(x) lowers to Type.GetTypeFromHandle(RuntimeTypeHandle) and hardcoded
  array initialization to RuntimeHelpers.InitializeArray(Array, RuntimeFieldHandle).

- Downgrade System.Linq.Expressions from Trust.Full to Trust.Partial:
  Expression.Compile() is runtime code generation. LINQ-to-objects (System.Linq)
  is unaffected.

Add WhitelistReflectionHardeningTests covering the still-allowed patterns
(typeof, type equality, hardcoded array init) and the newly-rejected ones
(reflection dispatch, denied parameter type).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Security (verified against a compiled-IL harness running these validators):
- WhitelistValidator scanned only top-level types and their immediate
  NestedTypes; depth-2+ nested types were completely invisible, so
  typeof(Assembly) + ((IReflect)t).InvokeMember("Load", ...) hidden two
  levels down re-opened the runtime Assembly.Load(byte[]) bypass even with
  System.Type locked down. Scan module.GetAllTypes() instead.
- ReflectionValidator matched System.Type.InvokeMember only by exact
  declaring-type name; ban System.Reflection.IReflect, System.Reflection.Binder
  and System.Runtime.InteropServices._Type (all expose InvokeMember), plus
  Delegate.CreateDelegate.
- Full-trust early return ran before generic decomposition, so a trusted
  container (RepeatedField<T>, List<T>, ...) laundered a denied type argument.
  Unwrap byref/array/generic-instance first; apply trust to leaf types only.
- Own-method parameter/return types were never scanned (the previous test
  passed only because Assembly has op_Equality). Scan the declared signature;
  regression test now uses Binder + 'is null', which emits no call.
- CSharpContractAuditor ran ACS validation (Activator.CreateInstance +
  BindService invoke = executing untrusted code in the node process) before
  checking static findings. Reject on static findings first.

Compatibility (fixes the CI corpus breakage):
- Skip parameter scanning for compiler-generated delegate constructors
  (object, IntPtr): every lambda in every contract references one, which
  produced ~17k 'IntPtr is not allowed' false positives. No hole: function
  pointers only enter via ldftn/ldvirtftn, whose operand is validated.
- Allow System.Linq.Expressions namespace for tree construction/inspection;
  Compile/CompileToMethod remain banned by ReflectionValidator.
- Allow Delegate.Combine/Remove/op_Equality/op_Inequality for events.

Tests: depth-2 IReflect rejection (whitelist + reflection validators),
own-signature param/return rejection, generic-container laundering rejection,
lambda/LINQ and safe expression-tree compatibility, Delegate.CreateDelegate.
@eanz17

eanz17 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Revision pushed (ee1a2e5) after an adversarial review of the previous head. All findings below were reproduced against a Mono.Cecil/Roslyn harness running the validators verbatim, then re-verified fixed.

Residual bypasses closed

  1. Depth-2 nesting bypass (critical): WhitelistValidator only visited top-level types and their immediate NestedTypes, while method validators recurse without limit. A payload in a depth-2 nested type — typeof(System.Reflection.Assembly) + ((IReflect)t).InvokeMember("Load", ...) — passed every validator and demonstrably loads a second-stage assembly at runtime. Fixed by scanning module.GetAllTypes(), and by banning System.Reflection.IReflect / System.Reflection.Binder / System.Runtime.InteropServices._Type / Delegate.CreateDelegate in ReflectionValidator.
  2. Trusted generic containers laundered denied type arguments: the full-trust early return ran before generic decomposition, so RepeatedField<Assembly> was accepted. Trust is now applied to leaf types only; generic arguments are always validated.
  3. Own-method signatures were not scanned: the previous parameter test passed only because Roslyn lowers a == null on Assembly to op_Equality. Parameter/return types are now scanned; the regression test uses Binder + is null, which emits no call.
  4. Auditor executed rejected code: AcsValidator (Activator.CreateInstance + BindService invoke) ran before findings were checked, so a failing contract's constructor still executed in the node process. Static findings now reject first. (Metadata-only ACS validation remains a TODO — noted in code.)

Compatibility (the CI breakage)

  • Compiler-generated delegate constructors (object, IntPtr) are exempt from parameter scanning — this was the ~17k IntPtr is not allowed false positives. No hole: function pointers only enter via ldftn/ldvirtftn, whose operand is validated.
  • System.Linq.Expressions gets an explicit allow rule for tree construction/inspection; Compile/CompileToMethod stay banned by ReflectionValidator.
  • Delegate.Combine/Remove/op_Equality/op_Inequality allowed for events.

Not addressed here (needs its own change): already-deployed contracts are loaded by CSharpSmartContractRunner without re-audit — closing that needs an audit-policy version keyed by code hash plus quarantine of failing registrations, and is consensus-sensitive.

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.27273% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.56%. Comparing base (9f729e2) to head (bc192a9).

Files with missing lines Patch % Lines
...p.CodeOps/Validators/Method/ReflectionValidator.cs 77.00% 23 Missing ⚠️
src/AElf.CSharp.CodeOps/ExecutionObserverProxy.cs 81.81% 2 Missing ⚠️
...ckPruning/NewIrreversibleBlockFoundEventHandler.cs 91.66% 2 Missing ⚠️
...odeOps/Validators/Whitelist/IWhitelistValidator.cs 83.33% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##              dev    #3643      +/-   ##
==========================================
- Coverage   90.62%   90.56%   -0.07%     
==========================================
  Files         680      686       +6     
  Lines       26298    26507     +209     
  Branches     2369     2390      +21     
==========================================
+ Hits        23833    24005     +172     
- Misses       2350     2381      +31     
- Partials      115      121       +6     
Files with missing lines Coverage Δ
src/AElf.CSharp.CodeOps/CSharpContractAuditor.cs 93.75% <100.00%> (+0.41%) ⬆️
...CodeOps/Validators/Whitelist/IWhitelistProvider.cs 100.00% <100.00%> (ø)
...el.BlockPruning/Application/BlockPruningService.cs 100.00% <100.00%> (ø)
...AElf.Kernel.BlockPruning/BlockPruningAElfModule.cs 100.00% <100.00%> (ø)
...rc/AElf.Kernel.BlockPruning/BlockPruningOptions.cs 100.00% <100.00%> (ø)
...nel.BlockPruning/Domain/BlockPruningInfoManager.cs 100.00% <100.00%> (ø)
...Elf.Kernel.Core/Blockchain/Domain/IBlockManager.cs 100.00% <ø> (ø)
...Elf.Kernel.Core/Blockchain/Domain/IChainManager.cs 100.00% <ø> (ø)
...ore/Blockchain/Domain/ITransactionResultManager.cs 100.00% <ø> (ø)
src/AElf.Kernel/KernelAElfModule.cs 100.00% <ø> (ø)
... and 4 more

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants