Security
The Expression Engine
Template expressions are not run through the Function constructor. Lune tokenizes, parses and compiles each expression into a closure tree, caches it by source text, and reuses it on every update. Two things follow from that: Lune runs under a Content Security Policy that forbids unsafe-eval, and an expression can only do what the engine implements.
Supported Syntax
The subset covers what bindings need:
- Literals and template literals.
- Arrays and objects, including spreads.
- Member access, optional chaining, calls, and
new. - The arithmetic, comparison, logical, bitwise, and assignment operators, plus
typeof,void,inandinstanceof. - Arrow functions.
- The
if,returnandthrowstatements, andlet/const/vardeclarations.
Loops, function and class declarations, async/await, and the delete operator are not part of the subset. Keep anything that needs them in a scope method, where it is ordinary JavaScript.
Arrow parameters and declarations shadow scope properties of the same name, which is worth knowing if you are moving from a with-based template engine:
<!-- `item` here is the arrow parameter, not a scope property named `item` -->
<span>{{ items.filter((item) => item.done).length }}</span>Identifier Resolution
An identifier is looked up in this order:
- Local bindings introduced by the expression (arrow parameters,
let/const/var). - The current scope, including everything inherited from parent scopes.
- The
$dataand$elaliases. - The allowed globals.
Anything else raises a ReferenceError. The built-in allow list is the standard library plus a few conveniences:
Array, BigInt, Boolean, Date, Error, Infinity, Intl, JSON, Map, Math, NaN, Number, Object, Promise, RangeError, RegExp, Set, String, Symbol, TypeError, WeakMap, WeakSet, alert, confirm, console, decodeURI, decodeURIComponent, encodeURI, encodeURIComponent, isFinite, isNaN, parseFloat, parseInt, structuredClone.
Always Rejected
These are refused when the expression is compiled, whatever the scope contains:
- Identifiers:
Function,WebSocket,Worker,XMLHttpRequest,document,eval,exports,fetch,global,globalThis,importScripts,module,navigator,process,require,self,setInterval,setTimeout,top,window. - Property keys:
__proto__,constructorandprototype, whether read or written. - The
deleteoperator. - Expressions longer than 1000 characters.
Together these close the classic routes back to dynamic code evaluation and to prototype pollution.
In development, a rejected expression produces a console warning and the binding is skipped:
[Lune] WARN - Potentially unsafe expression rejected: "return(document.cookie)" "document" is not available to expressions.(Single expressions are compiled as return(…), which is why the wrapper shows up in the message.)
Application Globals
Applications that reference their own globals from a template register them first:
import { allowGlobals } from "lune-js";
import dayjs from "dayjs";
allowGlobals({ dayjs });<span lu-scope="{ date: '2026-01-01' }">{{ dayjs(date).format('MMM D') }}</span>A few things to know about the registry:
- It belongs to the expression engine, not to an app: every app created from the same copy of Lune resolves the registered names, and there is no way to unregister one.
- Names are resolved when a binding runs, so registration works before or after
mount(). - The scope still wins. A scope property shadows a registered global of the same name.
- Names on the rejected list above cannot be registered —
allowGlobals({ fetch })throws.
It is available from every build. With a plain script tag it sits on the global Lune object:
<script src="https://cdn.jsdelivr.net/npm/lune-js" defer init></script>
<script>
Lune.allowGlobals({ dayjs });
</script>App data is the alternative: reach for allowGlobals when a value is shared by every region on the page, and for createApp({ … }) when it belongs to one app.
What the Engine Does Not Protect Against
The engine limits what a template can reach. It is not a sandbox for untrusted templates, and it is not a substitute for sanitizing input.
- Scope methods are ordinary JavaScript. A method on your scope can call
fetch, touchdocument, or do anything else — only the attribute expression that calls it is restricted. lu-htmlstill renders raw HTML. See the warning on that page.
XSS Vulnerabilities
Lune evaluates JavaScript expressions in the templates. This means if Lune is mounted on a region of the DOM that contains non-sanitized HTML from user data, it may lead to XSS attacks.
If your page renders user-submitted HTML, you should prefer initializing Lune using explicit mount target so that it only processes parts that are controlled by you.
You can also sanitize any user-submitted HTML for the lu-scope attribute.