Security
The Rule Repository runs inside your application: its script runs on your page, its requests travel through your endpoint, and your application tells it who the user is. Security is therefore shared. The repository secures its own requests, its own output and its own saves; your application secures who can reach the repository and who they are.
This page lists both halves: what the repository does by itself, what your application must do (with the reason for each item and what happens without it), and the settings you can tune. Read it before your first deployment, and give it to your security reviewer.
What the Rule Repository Does by Itself
- Every request carries an anti-forgery token bound to the signed-in user, so a page on another site cannot make a user's browser post to your endpoint as them.
- Every name, description and value is shown as text, never as markup, both in the repository's screens and in every Rule Editor. A rule or a field named
<img src=x onerror=...> shows those characters; nothing runs.
- Every database statement is parameterized. No text a user typed is ever part of a statement.
- Every action is checked again on the server against the user's permissions. Turning a button back on in the browser achieves nothing.
- Saves are guarded against lost updates. A save made from a dialog that is out of date is refused, and nothing is written.
- Test results cannot be forged. A saved test keeps a run only if the server sealed it for that user.
- A rule cannot call code its source object does not declare. The server rebuilds every rule from the stored source definition, so a crafted request cannot make a rule call a method on another class.
- Requests are held to limits on their size, the filters they carry, the options a list returns and the test runs under way at once.
- It runs under a strict Content Security Policy: no inline scripts or styles, no
eval, and optional Trusted Types.
What may be said about it, and what may not: the Rule Repository secures its own surface and relies on your application only to say who the user is. It does not protect a page that has a cross-site scripting hole of its own (script running in your page has the user's token), an application whose sign-in is broken, or a database that others can write to.
What Your Application Must Do
| # |
Do this |
Why, and what happens without it |
| 1 |
Require your application's sign-in on the repository endpoint. In ASP.NET Core, .RequireAuthorization() on its MapPost. |
The repository has no sign-in of its own. Without it, anyone who can reach the URL reads and changes rules. |
| 2 |
Fill CurrentUserId from the signed-in identity, never from the request. |
The token is bound to this ID, and the history records it. Without it, one user can act as another. |
| 3 |
Allow no cross-origin requests with credentials on the endpoint: no permissive CORS policy. |
Another site could read the first answer, take the token from it and post as the user. |
| 4 |
Accept JSON only, and cap the body size. Return 415 for any other content type. |
A cross-site form can post text/plain without the browser asking first. A huge body costs your server before the repository sees it. |
| 5 |
Give each user the permission set of their role, built on the server per request. |
The default is view-only. A set that is too broad lets users publish or delete what they should not. See Permissions. |
| 6 |
Show the repository's messages as text if you use notifyCallback: set them with textContent or your UI library's text binding, never innerHTML. |
Messages can contain names users typed. Without it, a rule name could run script for everyone who sees the message. Without notifyCallback, the repository shows them itself, as text. |
| 7 |
Let only your application write to the repository's database. |
The key that signs tokens and test runs is stored there. Whoever can write the database can forge them, and change the history. See The Database. |
| 8 |
Serve your application over HTTPS. |
Tokens, rules and data travel in every request. |
The Hardened Endpoint
An ASP.NET Core endpoint that does all of the above. The database provider is registered once, at startup, as a singleton (builder.Services.AddSingleton<IDbProvider>(...); see Connecting to the Database):
using CodeEffects.Rule.Common.Models;
using CodeEffects.Rule.Connection;
using CodeEffects.Rule.Repository;
const long MaxRequestBytes = 4 * 1024 * 1024;
app.MapPost("/api/rules", async (HttpContext http, IDbProvider dbProvider, ILogger<Program> logger) =>
{
// 4. JSON only, and a size cap
if(http.Request.ContentType == null ||
!http.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return Results.StatusCode(StatusCodes.Status415UnsupportedMediaType);
if(http.Request.ContentLength > MaxRequestBytes)
return Results.StatusCode(StatusCodes.Status413PayloadTooLarge);
string requestJson;
using(var reader = new StreamReader(http.Request.Body))
requestJson = await reader.ReadToEndAsync();
// 2. The user from the sign-in, never from the request
var userId = http.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if(string.IsNullOrEmpty(userId)) return Results.Unauthorized();
var config = new Configuration
{
CurrentUserId = userId,
CurrentUserName = http.User.Identity?.Name,
// 5. The permissions of the user's role
AccessMode = http.User.IsInRole("RulePublisher") ? ActionPermissionMode.AllowAllExcept : ActionPermissionMode.ViewOnly,
ActionList = new HashSet<ActionPermissionType>()
};
try
{
return Results.Content(Context.GetResponse(config, dbProvider, requestJson), "application/json");
}
catch(RepositoryPermissionException ex)
{
logger.LogWarning("Rule Repository refused {Action} to {User}.", ex.Action, userId);
return Results.Content(ex.ResponseJson, "application/json");
}
catch(RepositoryException ex)
{
logger.LogError(ex, "Rule Repository request failed: {ControlNumber}", ex.ControlNumber);
return Results.Content(ex.ResponseJson, "application/json");
}
})
.RequireAuthorization(); // 1. Your sign-in
No app.UseCors(...) policy applies to this route (3), and the application runs behind HTTPS (8).
The Anti-Forgery Token
- The client's first request of a page (Init) is answered with a token. Every later request carries it in its body, so it reaches the server through any transport your page uses:
fetch, a framework's HTTP client, or a WebForms page method.
- A token is valid for 24 hours and is bound to
CurrentUserId. Nothing about it is stored.
- A request with a token that is missing, expired, issued to another user or not signed by this repository is refused with a warning (CE10202: "This page has expired or could not be verified. Reload the page to continue."). Nothing is done. The user reloads the page.
- Your code does nothing for this: the client and
Context.GetResponse(..) handle it.
- Server-side callers, such as your own administration tool or an integration test, that build requests themselves get a token for the configuration's user with
Context.GetRequestToken(config, dbProvider), and put it in the request's Token.
string token = Context.GetRequestToken(config, dbProvider);
The MCP server does not use this token: it has no browser, and signs agents in through your application's OAuth. See Authentication and the Caller.
The Signing Key
The token and sealed test runs are signed with a key the repository creates on first use: 32 random bytes from .NET's cryptographic generator, stored in the database's SettingRecord table under SigningKey. No configuration is needed.
- To rotate it, delete that row. The next request creates a new key. Pages that are open need a reload; nothing stored depends on the key.
- A damaged key is CE10201, whose message says to delete the row.
- Servers cache the key for five minutes, and read it again at once when a signature does not verify, so a web farm follows a rotation on its own.
- The key lives in the database it protects. It stops another site and a user tampering in the browser; it does not stop someone who can read the database. Keep database access to your application alone.
Cross-Site Scripting and Content Security Policy
Both scripts, codeeffects.editor.js and codeeffects.repository.js, put every name, description and value on the page as text. Neither uses eval, new Function, string timers, inline event handlers or javascript: links, and neither writes inline style attributes or <style> elements: styles set while they run go through the DOM, which a policy without 'unsafe-inline' allows.
A strict policy that the repository runs under:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
Adjust script-src and style-src if you serve the Code Effects files from another origin, and connect-src if your endpoint is on another origin. In ASP.NET Core, add it with a small middleware:
app.Use(async (context, next) =>
{
context.Response.Headers["Content-Security-Policy"] =
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; " +
"connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'";
await next();
});
Trusted Types
A page that enforces Trusted Types needs two things for the repository:
- Pass
useTrustedTypes: true to $repo.init(..). Optionally name the policy with trustedTypesPolicy; the default is codeeffects-repository.
- List that name in the policy's
trusted-types directive.
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types codeeffects-repository
const repository = $repo.init({
id: "rules",
requestCallback: request,
useTrustedTypes: true
});
The Rule Editor needs no policy of its own. Without the option, or without the name in the policy, $repo.init(..) throws and says what to add. Every other script on the page must be Trusted Types-safe too; in development, turn off Visual Studio's Browser Link, whose script is refused on such a page. A page that does not enforce Trusted Types leaves the option off.
Request Limits
Every request is held to limits that stop a single user, or a broken script, from overloading the server. The defaults suit almost every installation. Raise one only when a refusal says it was reached and the work is legitimate.
RequestLimits property |
Default |
Refused with |
RequestLength: the longest request, in characters |
3 MB |
CE10205 |
PayloadLength: the longest stored payload (a draft's rule, a test's values and run, a value template), in characters |
1 MB |
CE10207 |
Filters: the most filters one grid request may carry |
50 |
CE10206 |
LookupOptions: the most options one list returns |
1,000 |
(the list stops there) |
TestRunsAtOnce: test runs under way at once in the process, for all users together |
8 |
CE10209 |
var config = new Configuration
{
// ...
Limits = new RequestLimits { TestRunsAtOnce = 16, LookupOptions = 2000 }
};
An AI Assistant attachment arrives in one request and may be larger than RequestLength: its own limit is AssistantSettings.MaxAttachmentBytes. Raise your endpoint's body cap to match when you allow attachments (see AI Assistant and AI Rule Editor).
Test runs also stop after 10 seconds, so a rule that calls a method that never returns cannot hold a thread for long.
Saves That Would Overwrite Someone's Work
When two people edit the same record, the second save would silently undo the first. The repository refuses it instead: a save made from a dialog whose record changed since the dialog opened is answered with CE10204 ("Someone else saved this while you had it open, so nothing was saved. Reopen it to see their change."), and nothing is written. This covers names and descriptions of source objects, source versions, rules and rule versions, drafts, tests and value templates. Rule versions need no guard: they never change once saved.
Tests That Call Real Code
A test runs a rule on your application's real classes. Methods a rule calls run for real unless the developer declared a test method to run in their place. Two permissions control this:
TestRun (61) lets a user run tests.
TestRunProductionCalls (66) lets a test call a method that has no test method, for real. Without it, a single run that would do so is refused, and a batch run skips such tests.
Declare test methods for anything with side effects (sending email, charging a card, writing records), and grant permission 66 only to those who should run real code. AI agents always ask the person for confirmation before such a run.
What the Rule Engine Loads
A rule written in the repository can only call members its source object declares: the server rebuilds every rule from the stored source definition and ignores any class a request names. The Rule Engine's own AssemblyWhitelist setting is left unset by the repository; set it in your evaluation code if your policy restricts which assemblies rules may load types from (see Assembly Whitelisting).
In a .NET Framework Application
The same duties apply. A WebForms page method or an ASP.NET MVC action that serves as the endpoint must require authentication ([Authorize], or a <location> rule in web.config), take the user from HttpContext.Current.User, accept only application/json, and cap the request with maxRequestLength in web.config. See The Server Endpoint.