Security
What hypersig checks so that a page and its actions resist what a visitor can send, why data from the browser cannot run code on the server, and what is left to the application.
What the page writes
| What could go wrong | What hypersig does |
|---|---|
| A value becomes markup or a script | Text and attribute values are escaped when the server writes them, and the runtime sets text with textContent. No public method inserts HTML that the builder did not write. |
A link runs a script, such as javascript: | El::href and El::src take a Link to a route or a SafeUrl, which holds only http:, https:, mailto:, or a relative URL. SafeUrl::parse checks a URL a visitor typed, and a SafeUrl field of the signals checks it again when it deserializes. attr and bind_attr panic for href, src, an on* handler, srcdoc, action, formaction, xlink:href, data, and http-equiv, in any case, and for a name holding anything but letters, digits, -, _, ., and :. An SVG animation whose attributeName is href panics too, and bind_attr does not write attributeName. |
| A URL sends the browser to another host | A Url comes only from Route::url. A leading or doubled / of a Rest value is written %2F, and /. goes in front of a path that an empty first segment would start with //, so no URL or link a route writes names another host. |
| A style value adds declarations or rules | A CSS string escapes every control character, ", \, and <. A bound style value that could end its declaration is left for the runtime, whose setProperty refuses it. raw refuses an escape, a comment, ;, {, }, <, and an open quote or parenthesis, and Stylesheet::new refuses </style at compile time. |
| The runtime runs code from the page | The runtime evaluates expressions from a fixed table of operators. It calls neither eval nor new Function, so a page served with Page::csp_nonce runs under a Content-Security-Policy without unsafe-eval or unsafe-inline. |
What a request must carry
An action, a stream, or an upload answers a request with an error before its handler runs, so a page on another site cannot make a visitor's browser call it with the visitor's cookie.
| Request | Answer |
|---|---|
Sec-Fetch-Site is not same-origin, same-site included | 403 |
A POST whose body is not application/json, which a form cannot send and a script on another site sends only after a preflight this server does not answer | 415 |
A GET without the Accept: text/event-stream the runtime sends, as from an <img> or a link | 403 |
An upload without the hypersig-upload header, which needs a preflight | 403 |
An upload longer than Upload::max_bytes, 8 MiB unless set | 413 |
| Arguments that do not deserialize into the action's argument type | 400 |
A script on another site can still send that Accept header with a GET, and a browser that sends no Sec-Fetch-Site lets it through, so a GET action changes nothing.
Data from the browser and code on the server
Some frameworks have let a request run code on the server: the request names a server function, a module, or a class, and the framework finds it and calls it while it reads the request. In hypersig the request has no way to name code, for these reasons.
- The router decides which handler runs from the request's path and method, and the routes are fixed when the program starts. A request cannot add a route or pick a handler by name.
- The request carries only the arguments, as JSON.
serde_jsonreads them into the argument type the action declared at compile time, and anything that does not fit gets a 400. The JSON has no form for a function, a reference to a server object, or a class to construct, so reading it builds data and calls nothing. serde_jsonstops at 128 levels of nesting, so a deeply nested body is refused rather than exhausting the stack.- The server evaluates only the expressions the application's Rust code built, to write the first HTML. It never evaluates an expression or a template that arrived in a request, and it reads no file named by one.
- The crates forbid
unsafe, so a malformed request cannot corrupt memory in them.
What the application checks
- Who the visitor is and what they may do. Every action's URL can be called directly, without loading the page, and the browser can send any value of the argument type, so each handler checks that the visitor may act on the arguments it receives. The
authexample reads the signed-in member with an extractor in each handler. - The values of the signals. The browser holds them and can change them, so a price, a role, or an owner the server relies on is read again on the server.
- The
Content-Security-Policyheader.Page::csp_nonceputs the nonce on the page's elements, and the application sends the header with a nonce of at least 128 random bits per response. - The host of a URL. A
SafeUrlmay name any host, so a URL a visitor typed suits a link the visitor follows, and not thesrcof a script or thehrefof a<base>. - Ids built from stored data. Rendering panics for two fixed ids alike, but two
Stringids alike render, and a link or a patch to that id reaches the first of them. Build an id from a value visitors stored, such as a tag name, together with the record's key, so that it stays unique. - Its own queries and files, such as SQL built from arguments, and a file name from an upload, which
UploadedFile::file_namereturns as the browser sent it.
How the checks are tested
Each refusal above has a test in the repository: the escaping and the attribute checks in the tests of the HTML builder and of the style values, the requests in crates/hypersig/tests/axum.rs and upload.rs, and the compile errors in the trybuild cases under crates/hypersig/tests/ui/fail. The crates have been reviewed with Trail of Bits' sharp-edges, insecure-defaults, rust-review, and differential-review skills, and each finding they confirmed was fixed, is listed above as the application's to check, or fails only the request that caused it. No outside security audit has been done.