日本語

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 wrongWhat hypersig does
A value becomes markup or a scriptText 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 hostA 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 rulesA 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 pageThe 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.

RequestAnswer
Sec-Fetch-Site is not same-origin, same-site included403
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 answer415
A GET without the Accept: text/event-stream the runtime sends, as from an <img> or a link403
An upload without the hypersig-upload header, which needs a preflight403
An upload longer than Upload::max_bytes, 8 MiB unless set413
Arguments that do not deserialize into the action's argument type400

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_json reads 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_json stops 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 auth example 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-Policy header. Page::csp_nonce puts 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 SafeUrl may name any host, so a URL a visitor typed suits a link the visitor follows, and not the src of a script or the href of a <base>.
  • Ids built from stored data. Rendering panics for two fixed ids alike, but two String ids 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_name returns 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.