Security
Last reviewed: August 2026.
Generative UI raises one question before any other. If a model produces the interface, and that interface runs on your infrastructure, what stops it producing something that executes code, exhausts the machine, or reaches data it should not?
Most generate-and-run systems answer that afterwards: a container, a timeout, an allow-list policed at run time. Fuaran answers it earlier. What a model emits is not code. It is a bounded, closure-free data structure that a host decodes and renders. There is no interpreter to escape, because there is nothing to interpret.
This page states that property precisely, names the public artefact behind each claim, and then says which host enforces which part of it today. The properties do not hold uniformly across the roster, and the conformance table is the part of this page worth reading first. A safety claim is only as good as its stated boundary, so the boundary is not an appendix here.
The claims, and what backs each
1. The emission is data, not code
The wire format is a finite tree of typed nodes serialised as canonical JSON. Every function-shaped slot in the model, an event handler, a data accessor, a callback, serialises to a fixed sentinel on the wire, and the decoder reconstructs no callable behaviour from it. A host decodes JSON into a typed tree and renders it. It never evaluates, compiles, or invokes what it decoded, because there is no decode path that produces a function.
That is legible directly from the format rather than taken on trust: the sentinel, the closed node vocabulary, and the absence of any script or raw-markup slot are all normative text.
Read it in the specification, and check it against the conformance corpus, the shared fixture set every codec host is held byte-identical to.
2. Decoding refuses rather than guesses
Undeclared keys are rejected, not ignored. An unknown case in a closed vocabulary is an error, not a fallback. Every refusal is a typed error with a stable code and a path, and the specification pins the order in which failures are reported, so conformant hosts surface the same first error for the same bad input. A malformed or hostile document yields a structured rejection rather than a guessed-at render.
The reasoning behind that choice, including what it costs, is in default-deny by shape. The codes themselves are enumerated in the error-code reference, and the corpus carries a reject family that pins the expected code and path for each.
3. The document's structure is bounded
Semantic strictness says nothing about shape. A decoder for a recursive format is a recursive walk, and a few hundred kilobytes of nothing but opening brackets will drive an unguarded host off the end of its stack. On several host languages that is not even a catchable condition, which turns it into a one-request remote kill.
Section 21 of the specification closes this as part of the format rather than as
a per-deployment choice: bounds on node depth, syntactic JSON depth, string
length, array length, and total node count, with a typed LIMIT_EXCEEDED
refusal that must not escape the host's declared error type, and must be
enforced on the way down rather than measured afterwards. The section also
records how the node-depth figure was derived, by measuring the true overflow
depth of each walk in the reference host rather than by choosing a round number.
This is the property with the largest gap between specification and roster, and section 21.5 of the specification is itself the honest record of it: it names which hosts have adopted the limits and what the others do instead. The conformance table below says the same thing in short form.
4. Every string reaching the DOM has a declared posture
A tree is data, but rendering it still writes strings into a document. The
language tier publishes a per-seam sanitisation contract covering text content,
markdown bodies, attribute values, attribute names, URL props, and the
Navigate route: which seam is escaped by the view layer, which is sanitised
explicitly, which is host-trusted, and where each check lives in the source.
Two parts of it are worth singling out because they are the ones most often assumed rather than checked. URL-valued slots are scheme-allow-listed by default deny, with protocol-relative destinations rejected in all four spellings a browser folds into an off-origin authority, including the backslash forms. That one is not a host convention: section 19 of the specification makes it a normative obligation on every rendering host, precisely because a per-host choice meant a tree vetted on one host was not thereby safe on another. And attribute names are gated by a positive character allowlist rather than a prefix rule alone, because HTML has no escape for an illegal character in an attribute name: a key that terminates its own name is not a mangled attribute, it is several attributes, one of which can be a live event handler.
The artefacts are section 19 of the specification for the URL floor, and the language tier's sanitisation contract for the rest, which also states, per seam, what its markdown sweep does not catch and why that is deliberate.
5. Capabilities are denied unless the host grants them
An emitted tree can express an intent to reach the host, to navigate, to write state, or to invoke a host-registered tool, but that intent stays inert until a policy gate matches it against an allow-list the host controls. The absence of an explicit permission is a denial rather than a gap, so an emission nobody anticipated is refused rather than permitted by omission.
That default was inverted in release 0.14.0 of the .NET host, and it is worth saying why rather than only that it happened. Until then the shipped runtimes returned "allow" from the gate, on the reasoning that the renderer is not the trust boundary and the host's own gate is. That reasoning was self-defeating: those runtimes are what an unconfigured host receives, so for an unconfigured host "the host's gate" was exactly the line returning allow. The same release closed the descriptor set, since four wire-survivable actions had been reaching their substrates without consulting the gate at all. Release 0.15.0 followed with the guest-mount boundary: a decoded mount is clamped to an outbound-only channel, a guest with no host-installed policy receives a runtime that refuses every capability, and the custom-renderer registry is scoped so a tree cannot select a renderer registered for a different surface.
Both are recorded as breaking changes, with the argument for each, in the .NET host's stability declaration.
Where each property holds today
The properties above are properties of the format and of the reference implementation. They are not yet uniform across the host roster, and a page that said "Fuaran guarantees" would be describing a plan rather than a state. What follows is the state.
| Property | F# (reference) | TypeScript | Python | Go | Rust | Swift, Kotlin |
|---|---|---|---|---|---|---|
| Closure-free decode, no callable reconstructed | Enforced | Enforced | Enforced | Enforced | Enforced | Inherited from the Rust core |
| Typed refusals for semantic violations | Enforced | Enforced | Enforced | Enforced | Enforced | Inherited from the Rust core |
| Structural limits with a typed refusal | Enforced | Not yet | Not yet | Not assessed | Not assessed | Follows the Rust core |
| URL scheme allowlist and protocol-relative rejection | Enforced | Enforced | Enforced | Enforced | Enforced | Exposed, not enforced |
| Markdown and text escaping floor | Enforced | Enforced | Enforced | Enforced | Enforced | Not applicable |
| Attribute-name character allowlist | Enforced | Prefix rule only | Prefix rule only | Seam absent | Prefix rule only | Not applicable |
| Dispatch gate denies by default | Enforced | Seam present, absent gate allows | No gate | No gate | No gate | Embedding app owns dispatch |
| Guest-mount clamp and scoped renderer registry | Enforced | Not yet | Not yet | Not yet | Not yet | Not yet |
Five notes, because the cells compress more than they should.
Structural limits are the real gap. The TypeScript parser and the
decodeNode entry point wrap the walk in no counter and no catch, so the
engine's RangeError escapes the declared result contract as a throw. The
Python decoder catches ValueError around JSON parsing, and CPython raises
RecursionError on deep nesting, which is not a ValueError, so it escapes the
same way. Go and Rust have not been measured, and the specification asks for
measurement before either adopts a figure. Until a host adopts the limits, a
deeply nested document is a denial-of-service vector against it, and a
transport-level request-body cap is the mitigation available to you today. The
corpus deliberately carries no limit fixtures yet, because a shared gate landing
ahead of the hosts turns every host's build red for a rule none of them has
adopted.
The URL floor has a known evasion, and it is in the specification, not only in
the implementations. Rule 1 of the floor strips leading and trailing
whitespace, and rule 5 then asks whether the first two characters are each a
slash or a backslash. Browsers, following the URL standard, strip tab, line feed
and carriage return from anywhere in a URL before parsing it. A destination
whose two leading slashes are separated by one of those characters therefore
passes rule 5 as an ordinary relative reference, and is then reassembled by the
browser into an off-origin authority. Every host implements the rule as written,
so every host including the reference is affected. The consequence is an
off-origin link or an off-origin subresource request that leaks a referrer,
rather than script execution: rules 2 to 4 already normalise control characters
out of the scheme candidate, so about:blank and about:blank destinations are
still refused. It is stated here because it is open, and a host relying on the
floor to hold a same-origin intent should know the shape of the hole.
The native surfaces hold a different contract. The Swift and Kotlin surfaces are render projections over the Rust core rather than independent codec hosts, so the decode-side properties are the Rust core's. They render into native views and have no HTML sink, which removes the direct script-injection seam by construction. What that does not remove is the destination seam: a link, an image source, or a navigation route still arrives from the tree, and an app that hands one to a system open call has a scheme-injection sink. Both surfaces therefore ship the URL policy as a function the embedding app calls at the moment it has a resolved value, rather than enforcing it at decode, because a binding may not resolve to a literal until render time. The obligation is the app's, and it is stated in the surface's own source.
The dispatch gate and the mount clamp are reference-tier properties. The TypeScript renderer carries the gate seam, but an absent gate allows, which is the posture the .NET host left in 0.14.0. The other hosts have no gate. If you are running a non-reference host, capability restriction is yours to impose.
The attribute-name seam is host-side, not wire-reachable. The decoder hard-codes the extra-attributes slot to empty, so a decoded emission cannot carry one at all today. The reachable surface is a host or adapter mapping its own untrusted data into a tree. The gate lives at render time rather than at the decoder so that it stays correct if that ever changes.
What this does not cover
Naming the boundary is the point of the section, not a disclaimer at the end of one.
- It protects the host, not the meaning of the output. The bounded tree says nothing about whether the generated interface is correct, appropriate, or well labelled. A model can emit a wrong number in a perfectly conformant tree. Generation quality and structural safety are different axes, and the evaluation is where the first one is measured.
- Content-Security-Policy is yours. The renderer sets no headers. A policy, and on a static host the caveat that a meta-tag policy carries no frame-ancestors or referrer directive at all, is the deploying host's to get right.
- The custom-renderer registry is a host trust boundary. A host may register its own renderer for a component the language tier does not ship. That closure is host-authored code and the host owns and escapes its output; the renderer does not police it. The decoder cannot construct such a closure from the wire, so an emitted tree can only select among renderers the host itself registered, and since 0.15.0 only those registered in the scope it renders under.
- Transport-level size limits are yours. The structural limits bound depth and count, not total bytes. A size cap cannot express "no more than 24 levels deep", and a structural limit cannot express "no more than 8 MB". They are complements, and only one of them is in the format.
- DOM clobbering and Trusted Types are out of scope. Author-supplied identifiers that collide with global names are an author trust boundary, and Trusted Types is an opt-in browser surface a consumer wires up in its own runtime.
- The op-stream hash chain is corruption detection, not tamper evidence. The chain is an unkeyed SHA-256 digest over canonical bytes. It detects accidental corruption and casual alteration, and it will not detect an attacker who can write the store, because anyone who can rewrite an entry can recompute the digests that follow it. Authenticity needs signing, which is a separate seam and is not shipped. Anywhere you see the chain described as tamper evidence, including in this project's own older prose, read it as this instead.
- There is no published fuzz or totality evidence yet. The totality claim rests on the specification's obligations and on the reference host's implementation of them, not on a published corpus of hostile inputs. A cross-host hostile-input family in the conformance corpus is the artefact that would settle it, and it does not exist today.
Every claim on this page names something you can read: a normative section, a published contract, a corpus, a recorded release decision. Where a claim would have to rest on a test rather than an artefact, it is not made here.
Reporting a concern
If you find a way that emitted output could reach code execution, unbounded cost,
or a capability the host did not allow, that is the finding worth having and we
want it. Report it privately through the relevant repository's security policy
under the fuaran-ui organisation rather than in
a public issue. Please allow a reasonable window for a fix before disclosure.
See also
- Default-deny by shape, the design argument behind claims 2 and 5.
- The wire format, the normative contract, including the resource limits and their per-host conformance status.
- The conformance corpus and the certification kit, for checking a host yourself.
- Hosts, for what kind of surface each implementation is.