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.