Security
Reporting a vulnerability
Please report security problems privately through GitHub's private vulnerability reporting rather than in a public issue. Include a page and data that show the problem.
Trust model
| Input | Trusted? | Why |
|---|---|---|
.hypra files |
yes | written by the site's developers, like any other source code |
| data passed when rendering | no | comes from databases, users and requests |
| the Accept header and the request URL | no | sent by the visitor |
Hypra's job is to make sure untrusted data can't change what a page does. A page's author can still write
unsafe code on purpose, for example with raw; the compiler reports those places (see below).
How data is written
Every place data can end up has its own rule. They're the same in the static build (interp.rs) and the PHP
runtime (php/runtime), and tests/differential.rs renders hundreds of random pages with tricky data in both to
make sure (set HYPRA_FUZZ_CASES for more).
| Where the data goes | What happens |
|---|---|
| text, and attribute values | HTML-escaped: &, <, >, " |
URL attributes (href, src, action, formaction, poster, cite, data, SVG's xlink:href, and the to, from and by of an SVG set or animate that changes a link) |
a javascript:, vbscript: or data:text/html URL (ignoring case, whitespace and control characters) becomes about:blank#blocked; characters URLs can't contain are percent-encoded; then HTML-escaped |
script and style |
never: their text is written as it is, braces included; a {name} that names data is a warning (SEC-005) |
JSON scripts (application/json, *+json, importmap) |
a JSON string with <, > and & written as \u003C, \u003E and \u0026, so the data can't close the element |
attributes that run code (on*, x-*, @*, :*, v-on:*, ng-*, hx-on*, srcdoc) or load it (src of script, iframe and embed, data of object, href of link, content of meta with http-equiv) |
HTML-escaped, and a warning (SEC-004): escaping keeps the attribute closed but can't make data safe as code |
raw |
written as-is, with a warning (SEC-002) |
the confirm question of an action |
a JSON string inside the HTML-escaped onsubmit attribute |
| Markdown and JSON outputs | the same values as the HTML, unescaped for Markdown and JSON-encoded for JSON; they're served as text/markdown and application/json, never as HTML |
What's checked when a page is compiled:
- URLs written in the page that run code are errors (
SEC-003), including"javascript:{x}"and URLs split by whitespace. - An SVG animation that changes a link can't take data in its
valueslist (SEC-007), since a list can't be checked as one URL. - Inline event handlers like
onclickare warnings (SEC-001). - Quoted attribute names must be valid names (
ATTR-008), so a name can't smuggle in another attribute. - Action names only use letters, digits, dots, dashes and underscores (
SEM-014), because they become URLs. - Names that end up in generated PHP (form, field and action names, paths) are fixed text, never data, and every string in generated PHP is written as an escaped PHP literal.
Forms and Laravel
- Hidden fields such as Laravel's CSRF token are only added to forms that post to this site. The check is an
allowlist: a URL counts as this site only if it has no scheme and doesn't start with
//, read the way browsers read it (tabs and newlines removed, leading spaces and control characters skipped,\treated as/). Sohttps://…,//…,/\evil.exampleandht<tab>tps://…never receive them, also when the URL comes from data. The Rust and PHP versions are tested against the same list of tricky URLs. - A button's
formactionmust be a fixed path on this site, andbase(href: …)must be fixed text (SEC-006): both could otherwise redirect a form, hidden fields included. - Quoted attribute names are lowercased like HTML does, so
"HREF"or"ONCLICK"get the same checks ashrefandonclick. - Format negotiation only changes a response that is a rendered
.hyprapage; other responses are untouched. Every negotiated response carriesVary: Accept, so shared caches keep the formats apart. - In strict mode (the default when
APP_DEBUGis on), data that doesn't match the page throws aDataErrornaming the.hypraline. TurnAPP_DEBUGoff in production, as with any Laravel error. - The compiler runs as a separate process with the view's path as an argument, never through a shell.
Limits
- Templates are trusted. A
.hyprafile from an untrusted source can do anything a PHP view can, starting withraw. Don't compile templates that visitors upload. - Data inside URLs isn't encoded per part.
a("/search?q={query}")percent-encodes characters URLs can't contain, but&or=in the data still start a new query parameter. Encode such values before rendering. - Attributes that run code are only warned about. If a page passes data into
onclickor Alpine'sx-data, the data is escaped as an attribute but runs as JavaScript. - Compiling is bounded, not cheap: nesting is limited to 256 levels and expanded pages to 200,000 elements, but a page near those limits still takes noticeable time and memory to compile. Compile trusted templates only.