foam3

FOAM Application Security

Secure by Construction

Conventional application security depends on developer discipline: training, coding guidelines and code review catch mistakes after they are written. FOAM puts the security controls in the framework, where they are written once, reviewed once and applied uniformly, so that an application’s security does not depend on every developer remembering to apply them in every feature. It does this in three ways:

Because features are generated deterministically from models, this also means:

Training and code review still matter, but as additional layers of defence rather than the primary control.

Structural Security — Immunity by Design

Most frameworks treat injection attacks as problems to be mitigated: sanitise inputs, escape outputs, document what not to do, and hope developers follow the rules consistently. FOAM takes a different approach — it eliminates the attack surface at the architectural level so that the normal API provides no path to injection in the first place.

XSS Immunity — U2 and Content Security Policy

U2, FOAM’s UI library, was designed in conjunction with Google’s security team during the Chrome Apps era. Chrome Apps enforced a strict security model that prohibited three things:

These constraints forced a fully component-based, code-wired architecture. U2 was built to satisfy all three by construction:

The result is that XSS is structurally impossible in a FOAM application. User input handled through the normal U2 API cannot become executable markup, because U2 never hands strings to the HTML parser.

Chrome Apps no longer exist, but FOAM retains the inline-script and inline-handler constraints through a self-imposed Content Security Policy. (The policy’s script-src does allow 'unsafe-eval'.) These security properties remain, independent of the platform that originally motivated them.

The policy is built from foam.core.http.csp.CSPDirective rows in src/cspdirectives.jrl. Its script-src does not include 'unsafe-inline', so a <script> tag written into an HTML page is not executed, and an inline listener such as onclick="code" is not evaluated either. This is a second, independent layer: even if markup were somehow injected into a page, the script in it would not execute.

Minimal client-side dependencies

The FOAM client has no third-party runtime dependencies. A few optional libraries are loaded from a CDN, and only when the component that needs them is used (via JsLib):

Each external script URL must also be allowed by the script-src directive, so a new library cannot be loaded without a reviewed change to cspdirectives.jrl.

SQL Injection Immunity — MLang

The same principle — build from objects, not strings — applies one layer down at the query layer.

SQL injection works because queries are built by string concatenation: user input lands inside the string, the database parser cannot distinguish data from syntax, and the injection succeeds. MLang makes this structurally impossible:

M.AND(
  M.EQ(Invoice.CUSTOMER_ID, customerId),  // a typed value, not a string fragment
  M.GT(Invoice.AMOUNT, userInput)         // typed and bounds-checked, not interpolated
)

There is no query string to inject into. The predicate tree is a graph of typed objects. When a ClientDAO sends a query over the network, it serialises the predicate tree to JSON and reconstructs it server-side — the server never receives a raw query string that user input could escape. When a JDBC DAO (PostgresDAO, MySQLDAO) executes against a SQL database, it generates parameterised queries from the predicate tree, with data always kept separate from query structure.

The Common Principle

Both protections follow from the same architectural decision made throughout FOAM: use typed object composition rather than string interpolation at every layer. There is no injection surface because there are no strings being parsed as code or markup anywhere in the normal execution path.

Layer Conventional risk FOAM’s approach Result
UI rendering XSS via template injection Builder API sets DOM properties, never innerHTML XSS structurally impossible
Query construction SQL injection via string concatenation MLang predicate objects, parameterised execution SQL injection structurally impossible
Runtime code Script injection via <script> tags or inline handlers (onclick="...") Strict CSP with no 'unsafe-inline' Injected inline script never executes

Application Security — Services and Permissions

FOAM application security centers around services and permissions to those services.

Services are known as CORE Services and their access configuration is modelled under the name CSpec (CORE Service Specification). The CSpec controls how a service is exposed to the rest of the system.

CSpecs are deployed in services.jrl journals and can also be created and manipulated at runtime.

Services, by default, are only accessible within the server application. They can be exposed to clients with CSpec property serve.

Services which are served, are authenticated. Authentication is enabled by default via CSpec property authenticate.

A client’s permissions are those of the *user that is associated with the session of the client connection.*

Autenticated services require the client (connecting user) to have permission

Unauthenticated services are open to all clients for reading.

Developers do not write REST or other network APIs by hand. The client stub and server skeleton for a served service are generated, and every request is checked by SessionServerBox, which calls CSpec.checkAuthorization() before the request reaches the service (src/foam/box/SessionServerBox.java:149, src/foam/core/boot/CSpec.js:332). No served endpoint can therefore be shipped without its authentication and authorization check.

Deny by Default

Every permission is denied unless explicitly granted. User groups descend from the basicUser group (src/groups.jrl), which grants only the minimum a logged-in user needs (src/groupPermissionJunctions.jrl). The same permission system governs both users and service-to-service calls, and can be applied at three levels: access to a service, individual objects (rows, via the model’s authorizer), and individual properties (columns, via PermissionedPropertyDAO), each described below.

Delegated Administration Without Escalation

Administrators can create users and groups and grant permissions, but cannot grant any permission they do not hold themselves:

A compromised or careless administrator account therefore cannot grant more access than it already has. These cases are covered by src/foam/core/auth/test/PreventPrivilegeEscalationTest.java.

Once a client connects to a service, security managing data manipulation is controlled at the model level (discussed below in Model Security).

The service that the CSpec describes is specified by one of the following properties, which resolve to a modelled class.

When the service is served, then the client property must be configured.

Additionally if the service is modelled as an INTERFACE, then boxClass property must also be provided. See /#flowdoc/Boxes.flow for a discussion of FOAM’s Box messaging protocol.

When the service is a DAO, then typically, a serviceScript is used to describe the DAO stack with EasyDAO (discussed below).

Model Security

Model security applies to both client and server operations, and is independent of the service’s accessibility.

Model security is implemented via permissions. The permissins are imposed at two levels: 1) the model itself controlling instance visibility and manipulation, and 2) model properties controlling visibility and manipulation of individual properties.

Model Level

Model level authentication is imposed via AuthorizationDAO, verifying the user has appropriate permissions for each CRUD operation.

Two interfaces control how the AuthorizationDAO is configured for each service:

  1. Authorizable
    • implemented by the model itself. AuthorizationDAO is configured with the AuthorizableAuthorizer which delegates to the model for permission checking.
    • A model would implement Authorizable when requiring behaviour different from the StandardAuthorizer.
      • see foam.core.auth.User as an example.
  2. Authorizer - implemented as a stand alone class. FOAM provides implementations for common scenarios that can be applied to most models.
    1. StandardAuthorizer (default)
      • verifies user has permission for each CRUD operation.
      • permission format (all lowercase):
        • modelname.read.id
        • modelname.create
        • modelname.update.id
        • modelname.remove.id
    2. Global Authorizers:
      • Global authorizers extend StandardAuthorizer and make particular CRUD operations unauthenticated.
        • GlobalReadAuthorizer: permission not required to read all DAO entries.
        • GlobalFindAuthorizer: permission not required to find all DAO entries.
        • GlobalPutAuthorizer: permission not required to create or update DAO entries.
        • GlobalCreateAuthorizer: permission not require to create new DAO entries.
        • GlobalFindOrPutAuthorizer: permission not required to find or put DAO entries.

EasyDAO

Typically the AuthorizationDAO is configured by EasyDAO.

EasyDAO is a support model designed to hide the complexities of, correctly and securely, configuring DAO decoration for a service (DAO stack).

EasyDAO properties relevant to Model level authentication:

Model Property Level (incomplete)

Model property authentication allows for fine grained per-property permissioning. It is imposed by PermissionedPropertyDAO.

Model property permissioning affects both client and server.

Typically, PermissionedPropertyDAO is configured by EasyDAO, with property permissioned.

EasyDAO properties relevant to Model level authentication:

Data Protection

Built-in Storage

FOAM’s default persistence is its own journalled DAO (JDAO over an in-memory MDAO) rather than an external database server. There is no database login, network port or administrative console: data is reachable only through the application’s DAO stack and its authentication, authorization and audit layers. Schema changes come from model changes, which go through code review, not from manual DDL.

Secrets and Passwords

System credentials, such as keys and passwords for external services, belong in the key store managed by foam.core.security.KeyStoreManager, not in journals, source code or configuration. User passwords are stored only as salted PBKDF2 hashes (foam.util.Password.hash).

Auditing

Auditing is a property of the storage layer, not something each service must remember to call.

When a DAO is persisted with JDAO (the default EasyDAO journal), the journal is itself the audit log. Before an update record, the journal writes a comment saying who made the change and when. Changes made by the system user get no comment, and consecutive writes by the same user within the journal’s commentWindowMs share one comment (writeComment_ in src/foam/dao/AbstractF3FileJournal.js):

// Modified by Jane Smith (1234) at 2026-10-30T14:02:11.517Z
p({"class":"com.example.Invoice","id":"INV-001","amount":150})

Because the journal is how the data is stored, this audit trail cannot be switched off without switching off persistence: there is no separate audit setting to disable or pause. Objects that implement LastModifiedByAware carry the same information on the record itself, so no comment is written for them.

A DAO persisted some other way, such as a JDBC DAO, has no journal and therefore no built-in audit trail. Auditing for such a DAO must be enabled explicitly, for example with EasyDAO’s history: true, which adds a HistoryDAO that writes a foam.dao.history.HistoryRecord (object id, user, agent, timestamp, and each changed property’s old and new value) for every update.

Failed logins are also recorded: FailedLoginAuthService logs each one and stores a LoginAttempt with the identifier, IP address and failure message in loginAttemptDAO.

Q & A (incomplete)

  1. What does setting authenticate on the cspec do? Is it safe to set it false?
    • authenticate comes into play when a CSpec is served (serve:true). A served service is exposed to clients and by default is authenticated. Authentication requires the user to have permission service.<CSpec.name>. An unauthenticated service (authenticate:false), will be visible to all clients.
    • If the CSpec is for a DAO, then security shifts to the model level (described elsewhere). It is unwise, without careful review, to have both CSpec authenticate:false and EasyDAO authorize:false.
    • If the CSpec is for a non-DAO service, then authenticate:false, makes the service accessible to all clients.
  2. Do I need to set both service.somethingDAO and something.read.* permissions?
    • If your model is using the default EasyDAO authorization, then both permissions service.<modelDAO> and model.read.* are required. If EasyDAO authorizer is configured with the GlobalReadAuthoriizer, then all clients can read all objects, but permissions to create, update, delete (ex. model.update.*) are required.
  3. What do I need to make my object Authorizable? Does it just work by default?
    • EasyDAO, by default, will decorate your model’s DAO with an AuthorizationDAO which will enforce permissions for each CRUD operation (read, create, update, and delete). If you require permission check behaviour different from the StandardAuthorizer, then have your model implement foam.core.auth.Authorizable and implement each of the authorizeOn... methods.
  4. How do the something.read.* permissions come into play if my object is authorizable
    • if a model implements foam.core.auth.Authorizable or, for a DAO, EasyDAO.authorize is enabled (which it is by default), then any read request by a client must have permission <modelname>.read.* or one or more <modelname>.read.id for each id of interest.
  5. What is the “global read permission” in the standardauthorizer class for?
    • The global read permission model.read.* is a wildcard permission which allows a user to see all ids (all instances) of the model in question. By default a user is restricted to only see instances they have been given explicit access to via a permission of the form model.read.id.
  6. How is the context configured in the authorizable callback for an Authorizable service? Does it have permissions scoped to the user making the request or system permissions or what? If I want to check something in a different dao that the user doesn’t have permission for which context do i use? Can I sudo?
    • Every client request is performed with the context scoped to the user. If server logic requires access the user does not have then generally it can use the context of this.getX(). The service instance context is set at creation time and is that of the system.
    • Auth.sudo is used when logic requires reduced scoped or scope of another user (other than system). For example, Approvals, approved by an operator but when put, the context of the user that the Approval affects is used so that rules of the post approval flow execute in the user’s scope.
  7. Also feature request: some sort of debugging mode or debugging flag i can put on my requests so I can check why i’m not seeing object I expected in a DAO. Debugging failed permission check is a bit of a black box.
    • start the server with the system property -Dfoam.box.debug=true to log the stack trace of every exception a service returns (DEBUG in src/foam/java/Skeleton.js).
    • TODO: implement support for this.

Other Security Topics (TODO)