Table of Contents

Accept rules you didn't write

RuleWright's rules are data, so a rule author can't run code on your server. That makes it reasonable to let analysts, customers or a UI write rules. It doesn't make every document harmless: patterns can be slow, documents can be large, and a rule can read anything reachable from the fact you pass it.

What RuleWright guarantees

  • No code in documents. The vocabulary is closed: operators, field paths and literals. Nothing in a document is compiled or evaluated as a string. The only way to call code is custom, and only functions you registered can be called.
  • Evaluation never throws on data. A hostile fact can't turn an evaluation into an exception path.
  • Parsing is bounded. Nesting is capped at 64 levels, and a number must be finite.
  • Regular expressions are time-limited, as below.
  • Evaluations are isolated. State never carries from one evaluation to the next, or between threads.

Regular expressions: time-limited

A MatchesRegex pattern comes from the rule author but runs against your data. A pattern like ^(a+)+$ backtracks exponentially on the wrong input. RuleWright caps every match, one second by default, and raises RegexMatchTimeoutException instead of letting it pin a thread:

using System.Text.RegularExpressions;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;
using RuleWright.Serialization;

RuleWrightEngine engine = new RuleWrightBuilder()
    .UseJsonReader(new SystemTextJsonReader())
    .UseRegexTimeout(TimeSpan.FromMilliseconds(100))
    .Build();

// (a+)+$ backtracks catastrophically on a long run of 'a' that does not end the way it expects.
LoadedRuleSet rules = engine.LoadRuleSet("""
    { "id": "suspicious-name",
      "condition": { "field": "Customer.Name", "operator": "MatchesRegex", "value": "^(a+)+$" },
      "actions": [ { "type": "setOutput", "target": "Flag", "value": true } ] }
    """);

var hostile = new Checkout { Customer = new Customer { Name = new string('a', 40) + "!" } };

try
{
    engine.Evaluate(rules, hostile);
}
catch (RegexMatchTimeoutException ex)
{
    Console.WriteLine($"{ex.GetType().Name} after {ex.MatchTimeout.TotalMilliseconds} ms: pattern {ex.Pattern}");
}

Output

RegexMatchTimeoutException after 100 ms: pattern ^(a+)+$

Pick a timeout that fits your latency budget with UseRegexTimeout, and treat the exception as a problem with the rule: log the rule id and disable or fix it.

What you still control

  • Pass a projection, not your domain object. A rule can read any public property or field reachable from the fact, including ones you never meant to expose (Customer.PasswordHash, Order.Supplier.BankAccount). Evaluate a purpose-built class or dictionary that holds exactly what rules may see.
  • Bound document size. Compilation cost grows with the number of rules and conditions. Cap the size of documents you accept, and the number of rules in a set.
  • Bound distinct documents per engine. The compiled cache is unbounded. If anyone can submit new rules, rebuild the engine periodically.
  • Your functions are your code. Their cost, their thread safety and what they touch are yours. Keep them pure, fast and total.
  • Review changes like code. Rules decide business outcomes. Keep them in version control, validate and test them in CI, and read the diff.

Reporting a vulnerability

See the security policy.