Skip to main content

Security Model

The views toolkit is built so that an author mistake fails closed instead of opening an injection or CSRF hole. This page collects the rules in one place.

Sinks never take raw HTML​

Every value the runtime paints goes through one of a small number of sinks:

SinkRule
Plain text (FieldFormat.TEXT)textContent.
FieldFormat.MARKDOWNParsed and copied node by node through an allowlist. Never innerHTML.
FieldFormat.SANITIZED_HTMLParsed with DOMParser into an inert document; allowlisted elements and attributes are copied with createElement. Never serialized back to a string, so the mutation-XSS class cannot arise.
Links (href, src)Scheme-checked. javascript:, data: and vbscript: are rejected by prefix, and a second colon-fallback rule rejects any value with a colon that is not an allowlisted absolute prefix (http:, https:, mailto: for href; http:, https: for src). The colon-fallback is the load-bearing rule: it still catches obfuscated spellings.

SANITIZED_HTML does not relieve you of sanitizing: the contract is caller-sanitizes-first, with the runtime's allowlist as a second layer.

Render allowlist​

Table-cell renderers are looked up in the client registry; an unknown id warns and falls back to the raw value. Field-grid fill sinks are stricter. SinkRenderAllowlist accepts only its frozen built-in ids (date, datetime, ts-zulu, bool, linked, truncate, json, decimal, tag, progress, pill, code), and CellPopover fields are further limited to SinkRenderAllowlist.POPOVER_TEXT_IDS (date, bool, decimal, datetime, ts-zulu). A pill on a fill sink is display only and never emits role="button".

Populator allowlist​

A region names a client-side populator function. PopulatorAllowlist validates RegionDef.populate against the region's allowedPopulators, so a catalog cannot name an arbitrary function; a blank populator name is rejected at validation time. A region's dataUrl must also be a same-origin path: no scheme, no leading //, and no .. segment (RegionDef.isSafeDetailEndpoint).

CSRF and the loopback boundary​

Row actions and bulk actions are state-changing requests. They are protected by two cooperating pieces in org.apache.juneau.rest.server.filter.

SynchronizerToken is a server-held secret minted in process memory, written into the pages the process serves (ViewTable stamps it as data-juneau-csrf), and required back in a request header on every state-changing request. It is deliberately not a double-submit cookie: cookies are scoped by host and ignore the port, so on a loopback port any other local server could plant a value the application would accept. Never put the token in a cookie or accept one read from a cookie. A restart invalidates every token already embedded in an open page.

LoopbackBoundary decides whether a request came from the page this process served:

CheckApplies toRejection
Host equals the configured authorityevery request421
Origin is exactly http://<authority>state-changing requests403
Sec-Fetch-Site absent or same-originstate-changing requests403
Content-Type base type is application/jsonstate-changing requests415
CSRF header equals the process's SynchronizerTokenstate-changing requests403

This proves the request came from your own page. It does not prove the user meant it. Intent needs a separate mechanism, such as a typed confirmation, an arming gate, or a per-action phrase naming the target. The views toolkit keeps those concerns apart on purpose: see WritePermit on Row Actions and Bulk Mutation.

A rejected request is answered with a JSON body, Cache-Control: no-store, and an X-Request-Boundary header naming the reason (for example CSRF_TOKEN_MISSING); page scripts match on that header.

LoopbackBoundary is for an app pinned to one loopback address. An app reached under a real host name, over HTTPS or behind a proxy uses CsrfBoundary, which the views toolkit works with unchanged. Its checks, the proxy recipes and the rules for non-browser clients are in REST Security. HTML form posts are not supported under either boundary, because a form cannot send the token header.

Any host that stamps a token into a response (a view host using ViewTable.CSRF_ATTR, RegionTable.of(region, csrfToken) or PageSpec.csrf(...)) must call RequestBoundaryFilter.markTokenBearing(response) before it writes the body, which sets Cache-Control: private, no-store. Otherwise a shared cache can serve one session's token to another.

A token is never embedded into a response that is already committed, because the response can no longer be marked non-cacheable. That includes csrf attributes an author put on the page's body element. The page still loads, and its row actions and other writes then fail visibly instead of sending a request the server would refuse.

Because RowAction.Method excludes safe methods and the server rejects a @Mutating operation bound to one at startup, a mutating action cannot be declared on GET.

Caps on async jobs​

The async-job registry enforces server-side caps that are security controls, not tuning knobs; see Async Jobs.

See also​