Security
A Jx site has two security surfaces. The
evaluation surface
comes with the format: a document contains executable code, in
${}
templates and function
body
/
$src
entries, so where that code runs decides the posture. The
data and session boundary
appears only once a site has a database or sign-ins, and decides what the deployed worker will do on a visitor's behalf.
Compiled sites run no eval
jx build
produces plain HTML/CSS plus per-island ES modules. It does
not
emit
new Function
or
eval
. A
${}
template is spliced verbatim into an emitted module as a real template literal, and expressions and statements lower to genuine JavaScript. So a compiled static or island page runs under a
strict Content-Security-Policy with no
'unsafe-eval'
. This is enforced by a compiler test (
packages/compiler/tests/no-eval.test.ts
).
Ship compiled output to production and the eval requirement disappears, which is the main reason the compiler exists.
The interpreter needs
'unsafe-eval'
The interpreting runtime, meaning the dev server, the Studio canvas, and
@jxsuite/runtime
used directly as a library, compiles
${}
templates and inline
body
functions with
new Function
on the fly. Any page that hosts the interpreter must allow
'unsafe-eval'
in its CSP.
${}
templates are
full JavaScript
, not a sandbox. A template has the component's
state
in scope, but also the entire global environment, and it can assign or call side effects. Do not render a template built from untrusted input in the interpreting runtime.
This is two settings, permanently, rather than one with a fix pending.
A compiled site never needs
'unsafe-eval'
; a page hosting the interpreter always will. The interpreter
is
the code that compiles expressions as it reads them, so "remove eval from the runtime" would mean removing the interpreter. Ship compiled output to production and the requirement is simply not there.
Trusted Types
Jx takes the half of Trusted Types that applies to it and declines the half that cannot.
Nothing Jx ships writes
innerHTML
.
The runtime and the compiled element modules clear elements with
replaceChildren()
instead, which is identical behavior and not a DOM injection sink. That is a smaller surface in every site you build, whether or not any policy is enforcing.
Studio's markdown goes through a policy that refuses.
The assistant's rendered markdown is the one place a string becomes markup in Studio's own window, and it passes a policy whose
createHTML
throws, naming what it found, if anything script-shaped survived sanitization. Its
createScript
and
createScriptURL
refuse outright.
Enforcement is declined, not pending.
require-trusted-types-for 'script'
gates
eval
and
new Function
as well as DOM injection, and the interpreter
is
those calls, in the canvas and in Studio's window alike. Enforcing would mean a policy that passes scripts through, which satisfies the API and defends nothing. Studio's remaining injection sinks belong to the libraries it bundles, which no policy of Jx's can route.
This is why the canvas and the shell keep separate policies permanently, and it works only because the canvas is a page with its own response: a frame loaded over
http
gets its own policy, while a
srcdoc
frame inherits its parent's.
Treat documents as code
A Jx document is executable input. Loading and rendering an untrusted document in the interpreter runs its code;
compiling
an untrusted document runs its code at build time (template text becomes code in the bundle). Give a
.json
document the same trust you would give a
.js
file from the same source.
If you compile documents you did not author, such as user-submitted content merged into the tree, sanitize or escape
${
sequences first.
The data and session boundary
A site with a database and sign-ins has a second security surface: the
/_jx/*
routes the
generated worker
serves. Nothing there trusts the browser.
Every table declares its own rules.
A
data
table's
permissions
carry one rule per action (
read
,
insert
,
update
,
delete
), each of them
public
,
none
,
authenticated
,
owner
, or
role:<name>
. The defaults are read
public
and every write
none
, so a table you have just declared is readable by anyone and writable by no one until you say otherwise. Rules are evaluated on the server for every request; there is no client-side check to bypass.
Authorization fails closed.
Anything beyond
public
and
none
needs the auth extension: it mounts first and publishes the session hooks the data mount authorizes against. Without it those rules deny: a missing auth mount answers 401, never a silent grant. The session lookup itself is fail-closed too: an unreachable auth backend reads as signed out rather than as trusted.
Ownership cannot be forged.
When a table declares an
ownerField
, the server stamps that column with the signed-in user's id on every session-granted insert, and scopes
owner
reads and writes to rows that match, so the value in the request body is not consulted. Roles work the same way: they live in the
user
table's
role
column, which sign-up input can never set.
Secrets stay names.
project.json
records env-var
names
(
urlEnv
,
secretEnv
,
clientIdEnv
), never values. Values reach code only through the server's
env
: from the git-ignored
.dev.vars
locally, and from your host's secret store in production. That is also why a
timing: "server"
entry is the right home for anything holding a credential. See
Auth and secrets
.
A table with
insert: "public"
is an open write endpoint: anyone on the internet can post rows to it, and there is no rate limiting or CAPTCHA in the contract today. Prefer
authenticated
inserts unless you knowingly want a public drop-box.
The extension-author side of the same model, what a mount may and may not do, is Security and secrets .
The dev server's network controls
The dev server binds loopback by default and gates its remote-code-execution routes (
/__jx_resolve__
,
/__jx_server__
), extension mounts, and the whole
/__studio/*
API behind an Origin/Host check, with realpath path containment. See
Dev server internals
and
@jxsuite/server
§4.2 for the full model.
Related
References :
$refbinding vs.${}templatesDev server internals : the network security model
Auth and secrets : sessions, roles, and where secret values live
Data tables : where the permission rules are declared
Security and secrets : the same boundary from an extension author's side