Skip to content

Expression evaluation security review with non-subtractive guardrails (#500)#984

Merged
JayVDZ merged 9 commits into
mainfrom
feature/expression-security-review
Jul 14, 2026
Merged

Expression evaluation security review with non-subtractive guardrails (#500)#984
JayVDZ merged 9 commits into
mainfrom
feature/expression-security-review

Conversation

@JayVDZ

@JayVDZ JayVDZ commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes the "DynamicExpresso input path review" gap from the OWASP Top 10:2025 assessment (A03/A05, gap 5). Part of #500.

The deliverable is a documented security review, engineering/EXPRESSION_SECURITY.md, plus two strictly non-subtractive guardrails. Per the product owner's constraint, nothing in this PR changes what any legitimate expression can do; a 46-test functionality regression suite (expressions taken verbatim from the customer docs, covering every built-in function) proves it.

Review findings

  • Input paths: only SyncRuleMappingSource.Expression is live today (Administrator-only via the REST API, the Attribute Flow tab, and New-JIMSyncRuleMapping). ObjectMatchingRuleSource.Expression and ExampleDataTemplateAttribute.Expression are model-ready but have no reachable authoring surface yet (verified: no DTO/UI editor/cmdlet populates them, and seeded templates carry no expressions); they inherit the evaluator's guardrails automatically if wired up later.
  • No untrusted-input path: attribute values flow through the get-only mv/cs accessors as data, never as expression source; hostile-looking values are proven inert.
  • What is blocked (proven by 28 security tests, not asserted): chained reflection (.GetType().GetMethods(), .Assembly, typeof(double).GetMethods()) throws ReflectionNotAllowedException; unregistered types (Process, FileStream, Environment, Activator, AppDomain) fail as unknown identifiers; assignment has no settable target. Validate rejects everything Evaluate rejects, so protection applies at save-time too.
  • Open finding, accepted as documented (validator decision): a bare mv["a"].GetType() parses and evaluates, returning an inert System.Type (DynamicExpresso's reflection blocker rejects anything chained onto a Type, and Type.Name is its one documented allowed member). Chaining is blocked, the value grants no capability, the author is a trusted Administrator, and blocking it would need brittle custom parse inspection; documented in the review rather than hardened.

Guardrails added (both TDD, red first)

  • MaxExpressionLength (10,000 characters) across Evaluate/Validate/Test: a paste-bomb sanity bound roughly two orders of magnitude above the longest documented real expression.
  • MaxCompiledExpressionCacheSize (1,000 entries) on the previously unbounded static compiled-expression cache, which ad-hoc expression-tester usage grew forever; clear-and-log on reaching the bound (recompilation is sub-millisecond).

Rejected measures are recorded in the review with rationale: function/operator allowlists, removing default reference types, and evaluation timeouts all impair legitimate or future transformation capability for no matching risk reduction against a trusted-author threat model.

Validation

  • dotnet build JIM.sln: 0 errors, 0 warnings. dotnet test JIM.sln: 3,436 passed (+80 new), 35 skipped (category-gated), 0 failed.
  • TDD evidence: guardrail tests written first and failed for the right reason (missing constants) before implementation.
  • DynamicExpresso claims verified against the pinned 2.19.3 source, then proven against the real evaluator by unit tests.

Note: this branch and #983 both move the OWASP assessment doc to plans/doing/ and touch the same COMPLIANCE_MAPPING row; whichever merges second will be brought up to date with main and reconciled before merge.

Docs: n/a - internal security review; the changelog entry is 🔒 and behaviour is unchanged for administrators.

🤖 Generated with Claude Code

… guardrails (#500)

- New engineering/EXPRESSION_SECURITY.md: threat model, input path
  inventory, verified DynamicExpresso exposure/blocking, guardrails,
  rejected measures, and one documented open finding (bare GetType()
  returns an inert Type; all chained reflection is blocked)
- DynamicExpressoEvaluator: 10,000-character expression length ceiling
  and a 1,000-entry bound on the static compiled-expression cache
  (previously unbounded via the ad-hoc expression tester); both TDD
- 28 security proof tests (reflection/type escapes, hostile-looking
  inputs) and a 46-test functionality regression suite proving every
  built-in function is unaffected by the guardrails
- OWASP assessment moved to engineering/plans/doing/ with gap 5 marked
  implemented; COMPLIANCE_MAPPING updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JayVDZ
JayVDZ merged commit 1b59f7b into main Jul 14, 2026
18 checks passed
@JayVDZ
JayVDZ deleted the feature/expression-security-review branch July 14, 2026 12:06
JayVDZ added a commit that referenced this pull request Jul 14, 2026
#1014)

All five remediation gaps are now merged (#1001 rate limiting, #1002
security headers/CSP, #983 NuGet lock files, #984 expression security
review, #1013 security audit events), completing the assessment's
remediation programme. Move the assessment document to plans/done per the
plan filing convention, set its status to Done with the deferred-items
note, and repoint the three engineering documents that referenced the
plans/doing path.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant