Security
What is handled
Section titled “What is handled”Interpolated values are never markup
Section titled “Interpolated values are never markup”A prop value becomes an attribute value, through setAttribute semantics and
HTML escaping. There is nothing to escape by hand and no way to forget:
// Interpolated values are attribute values, never markup. There is// nothing to escape by hand and no way to forget.return alacris.E("x-demo"). Prop("label", `" onload="alert(1)`). Prop("payload", map[string]string{"html": "</x-demo><script>"})<x-demo label="" onload="alert(1)" payload="{"html":"\u003c/x-demo\u003e\u003cscript\u003e"}"></x-demo>Values are attribute values, never markup.
The JSON prop is the interesting case: quotes inside it are escaped so the
attribute cannot end early, and the <script> in the payload is text when it
arrives.
alacris itself never parses an interpolated value as HTML either: child
bindings become text nodes and attribute bindings go through setAttribute,
so the guarantee holds on both sides of the boundary.
Inline CSS is checked, not escaped
Section titled “Inline CSS is checked, not escaped”Style and Var refuse anything that could end the declaration, end the
attribute, or open a comment:
.Var("bg", "red;position:fixed") // ';' is not allowed.Var("bg", "red/*x*/") // comment markers are not allowed.Var("bg", "url(javascript:alert(1))").Var("bg", "expression(alert(1))")An error names the character and the property. A sanitizer that silently substitutes a placeholder would give you a page that looks wrong for reasons you cannot see.
The import map cannot be closed early
Section titled “The import map cannot be closed early”Entries are JSON-encoded with Go’s default HTML escaping, so <, > and &
become < and friends. A hostile import path cannot end the <script>
element.
Structural validation
Section titled “Structural validation”Tag names are checked against the rule customElements.define enforces, and
attribute names against what can appear in a start tag. Both fail the render
rather than producing HTML whose structure differs from what you wrote.
Action payloads
Section titled “Action payloads”- The body is capped at
MaxDetail(64 KiB), enforced withhttp.MaxBytesReader. Binddecodes withDisallowUnknownFields, so a payload that does not match the declared detail is refused rather than arriving half-filled.- Cross-origin posts are rejected by comparing
Originagainst the requestHost.Options.AllowOriginoverrides that when you need to.
Session credentials
Section titled “Session credentials”Both halves are 18 bytes from crypto/rand, base64url. Long enough that
guessing is not a strategy, and generation panics rather than falling back to
anything weaker if the OS has no randomness. The client token is compared in
constant time, because that comparison is the authorisation decision.
Cross-site request forgery
Section titled “Cross-site request forgery”The capability is a cookie, so the browser attaches it to whatever a page can
cause. Three things stand in for the secret that used to sit in the request
body, and they are described in full on the
wire protocol page:
SameSite=Lax, a required application/json content type, and the page id as
a double-submit token checked against the cookie.
Cross-origin is off unless Options.AllowOrigin names the origins you trust. A
credentialed endpoint that reflects whatever origin it is sent is an open door,
so the headers that make cross-origin work are only sent for an origin that was
allowed, and the response never uses *.
Content Security Policy
Section titled “Content Security Policy”No eval, no new Function, anywhere in the runtime or the client. Two Trusted
Types policies are registered: alacris for template parsing, alacris-live
for SetHTML.
Content-Security-Policy: trusted-types alacris alacris-live; require-trusted-types-for 'script'; script-src 'nonce-{{nonce}}';Config.Nonce, or templ.WithNonce on the context, puts the nonce on every
script tag the library emits.
What is yours
Section titled “What is yours”Two things to know about
Section titled “Two things to know about”Patches are not authorised per element. A session can be told to write any prop on any element on its own page. That is the same page the server rendered, so a handler must decide whether an action is permitted before patching, not rely on the page not asking.
Errors do not reach the browser. A handler returning an error logs it and
answers 500; the response body is a fixed string. If a user should see
something, send a patch saying so in the page’s own terms. It stops internal
detail leaking through an error path nobody was thinking about.
Desktop loopback
Section titled “Desktop loopback”The app host binds 127.0.0.1 and rejects a Host header that is not that
address. Run also issues a host token the webview presents as an HttpOnly
cookie, so another local process that can reach the port cannot create a
session. The live cookie is still required. Do not bind 0.0.0.0. Details:
Desktop apps.
Reporting
Section titled “Reporting”Security issues in this module go to its security page. For the runtime itself, see alacris.