May 2025 (version 2.25.05.09)
reviewed: 7 October 2025
Untrusted Expression Evaluation
When developing systems, we often process user provided expressions on the backend server. These expressions typically take the form of an <@bo/attribute/expression> attribute where the data type is Expression, usually on a reference data BO, to determine whether an entry is active or to execute operations such as a count on a dashboard entry etc. In the case of dashboards, we may also define an text attribute that specifies the WHERE clause of the dashboard query. This where clause can include {} inline expressions supplied by the user who created the dashboard entry. These expressions are then evaluated server side during dashboard execution.
Although these attributes are generally limited to reference data, where access is restricted to defined users and groups, they still present a potential security risk. For example, an active expression should not require or be granted access to sensitive handlers such as the fileSystem, httpRequests, smtp, ftp handlers etc.
To address this concern, we have introduced the concept of untrusted expression evaluation alongside privileged functions and handlers. A privileged function or handler is one that is explicitly restricted to trusted environments. These functions are not permitted in untrusted contexts, ensuring that sensitive functions can only be used where appropriate security guarantees are in place.
The runtime defines what constitutes a privileged function or handler. Generally, these include operations that access the file system, network, descriptors, or directly interact with the database; essentially, the areas where elevated permissions are required. All standard functions typically used in reference data expressions and WHERE clauses remain available, ensuring that common use cases are fully supported in untrusted evaluation contexts.
A list of the privileged handlers and functions can be found at the end of this release log.
Even evaluating an expression attribute a developer will typically do something like the following:

For backward compatibility, the eval() function will continue to operate in a trusted context, as it has in the past. This ensures that the introduction of untrusted expression evaluation does not affect existing attribute expressions. Expressions already in use will continue to be evaluated as trusted, preserving current behaviour and avoiding disruption.
A new attribute property called evalSafe has been introduced. This property functions similarly to the htmlSafe property and defaults to false(), meaning attributes are treated as unsafe (untrusted) by default. To support this, a new bo.attrEval() function has been added. This function evaluates the attribute expression while considering the attributes evalSafe property, and by default, it evaluates the expression in untrusted mode

As such, developers are encouraged to review their use of eval() within applications and transition to the new bo.attrEval() function where appropriate. The evalSafe property should only be explicitly set to true for attributes whose expressions require access to privileged functions. This approach helps ensure a more secure and controlled execution environment by default moving forwards.
A new untrusted script statement block has also been introduced, allowing .cms statements to be explicitly wrapped and evaluated within an untrusted environment.

All {} inline expressions used within WHERE clauses are now evaluated in untrusted mode across the runtime. This behaviour is non configurable. There is no justifiable use case for requiring privileged functions or handlers within a WHERE clause expression. However, if such functionality is absolutely necessary, the required values can still be safely concatenated into the WHERE clause server-side, outside of the untrusted expression context.
Tooling
- The runtime and its components now include formal versioning
- The version follows the format 2.YY.MM.DD+HHmm, reflecting the year, month, day, and build time
- The assembly version remains at 1.0.0.0 to maintain compatibility and prevent breaking changes in dependent applications
- The file version remains 2.0.0.0 for now, but may be updated inline moving forwards

- A new `app.runtimeVersion()` function has been added, which returns a property bag containing the version details

- Improved documentation for access modifiers

New Features
- A new
ddlfunction handler has been added, deprecating the olderdevUtilfunctions.

- Added
tdg.probability()to retrieve values based on weighted probabilities.

- Added
tdg.pk()for retrieving a valid random primary key from a specified entity, with an optional where clause.

- Added BO descriptor support for getting and setting an attributes
testDataValue

Enhancements
- Added
excludedsupport to<@page/list/item> - Added
excludedsupport to<@page/data/form/tabs/tab> - Added
tooltipsupport to<@page/list/item> - Added
tooltipsupport to<@page/tabs/tab> - Added T-SQL
GObatch separators toCREATEandALTER DDLstatement generation.- This improves Microsoft SQL Server scripts that require explicit batch boundaries for execution
- The
mul()function now supports an unlimited number of parameters- This now matches
sum()andsub(), in allowing more than the basic two parameters
- This now matches
httpRequest.delete()now support sending a body payload as part of the DELETE request- Previously DELETE requests could only include headers and query parameters, but now you can send a request body, just like in other HTTP methods such as POST or PUT
<@page/data/table>exports now use theStreamLowMemoryiterator mode by default, greatly reducing the server RAM usage for large data exports- The
page/base:headerresource has been updated to a fully named and pathed property bag- This allows greater flexibility when overriding the header on the application or layer level, allowing for greater control of the positioning of custom items within the header
- The current
headerIncludelogic has not changed, and will continue to work, however we recommend using the new approach moving forwards
Header include:

- Support has been added for quoted property bag item names, enabling the inclusion of characters that were previously illegal in item names

json.pb2json()now supports an optional qualified:qualifiedparameter to omit @qualifier properties from the output JSON
Standard (Qualified) Output:

Unqualified Output:

- Added support for optional HMAC encoding, to allow for Base64 encoding

- A BOs attributes persist statuses are now set to LOADED after an INSERT
- This simulates that the BO has just been loaded from the data source, mirroring the current UPDATE logic
Fixes
bo2bowas incorrectly turning off validation on the from BO, instead of the to BOpb.dump()was incorrectly converting empty property bags <> tonull()- The full table name alias is now used when generating alias for inline views
- BO descriptor unquiet constrains are now only applied on INSERT if the resolved group has attributes
- Invalid attribute names in BO descriptor attribute groups no longer prevent DDL generation for the affected BO
- The script context stack was not being popped on SQL query builder exceptions
- Previously when a parsing error occurred in a WHERE clause, the script context stack was not properly unwound, resulting in misleading error messages
- Setting a none existent session context entry to
null()would result in the a broken data source connection - The following qualifier properties have been updated to no longer explicitly handle the provided values as
<@text>qualifiers:@page/link: confirmation@page/calendar/link: confirmation@page/table/link: confirmation@page/input/radio: label@page/input/select: label@page/javascript/setTitle: section@page/link/target/modal: title
Privileged Handlers
imappop3smtpftpsftpwebSockethttpRequestfileSystemtextFiledocumentemlexcelworddocxspreadsheetcsvimageresponsecachedevUtilsshellsqltracetxsessionhosterror
Privileged Functions
-
request.saveFile() -
qs.create() qs.set*()qs.clear*()-
qs.remove*() -
encrypt() sb.addTarget()app.reinitialize()app.getFileInfo()process.start()process.terminate()process.impersonate()language.set()page.render()-
test.call() -
boDesc.parse() boDesc.add*()boDesc.remove*()-
boDesc.set*() -
bo.addAttrError() bo.addError()bo.delete()bo.insert()bo.update()bo.persist()bo.setAlias()bo.setAttr()bo.writeAudit()