Docs
Escaping and raw HTML
How Proa encodes dynamic values, and the two ways to opt out.
Proa's primary security job is output encoding: making sure a dynamic value cannot break out of its HTML context and execute. Every value in html! is escaped before it reaches the buffer, with no opt-in.
let user_input = "<script>alert('xss')</script>";
html_sync! {
<p>{user_input}</p>
}
// <p><script>alert('xss')</script></p>
Encoding contexts
Proa escapes per the OWASP XSS Prevention Cheat Sheet, and the context decides the rule:
| Context | Characters escaped | OWASP rule |
|---|---|---|
| Text content | &, <, >, U+00A0, U+0000 | #1 |
| Attribute values | &, <, >, ", ', U+00A0, U+0000 | #2 |
<script> content | &, <, >, plus </script, <script, and <!-- masked with \ | Tag breakout |
<style> content | &, <, >, plus </style, <style, and <!-- masked with \ | Tag breakout |
All attribute values are always double-quoted. There is no code path that emits an unquoted attribute.
Dangerous URL schemes
javascript: and vbscript: execute arbitrary script when navigated to, and so do the executable data: types (text/html, application/xhtml+xml, application/xml, image/svg+xml). Proa blocks all of them in every URL-bearing attribute: href, src, action, formaction, poster, ping, cite, data, background, longdesc, manifest, and both spellings of xlink:href. The check runs during macro expansion and produces a compile error:
html_sync! {
<a href="javascript:alert(1)">"Click me"</a>
}
// error: dangerous URL scheme in `href` attribute.
The comparison normalizes the value the way a browser does before parsing it, so the usual evasions fail too. ASCII whitespace and NUL are skipped throughout the scheme (not just at the leading edge), which catches java	script: and \njavascript:, and one pass of numeric character reference decoding runs first, which catches javascript:alert(1) and its zero-padded variants.
Dynamic URLs get the same check at render time. A compile-time error cannot see href={user_supplied}, so URL-bearing attributes route through a runtime guard using the identical normalization. In debug builds a dangerous value trips a debug_assert!; in release builds the attribute renders empty rather than dangerous.
That guard is a backstop, not a substitute for validation. It only recognizes executable schemes, so an open redirect or an attacker-chosen host still reaches the page. Validate user-controlled URLs in application code.
Related: when you write <a target="_blank">, add rel="noopener noreferrer" unless you deliberately want an opener relationship. Without it the opened page can redirect your original tab through window.opener, reverse tabnabbing. Modern browsers default to noopener, but being explicit preserves behaviour in older clients.
Dynamic values inside script and style
Proa emits a compile-time warning when a dynamic expression appears inside <script> or <style>:
html_sync! {
<script>
{some_dynamic_value} // warning: OWASP XSS: dynamic expr in script/style
</script>
}
HTML escaping is not sufficient here. JavaScript and CSS are separate encoding contexts needing JS-specific and CSS-specific encoding, which an HTML engine does not provide.
Pass the data through a data-* attribute instead, and read it from the DOM:
use proa_core::StaticRaw;
html_sync! {
<div id="app" data-config={serde_json::to_string(&config).unwrap()}></div>
<script>
{StaticRaw(r#"
const config = JSON.parse(
document.getElementById('app').dataset.config
);
"#)}
</script>
}
The attribute value goes through attribute escaping, and the script reads it via the DOM. No injection path.
The two escape hatches
StaticRaw takes a compile-time constant. No user data can reach it at runtime, so it is inherently safe:
use proa_core::StaticRaw;
StaticRaw("<svg viewBox='0 0 24 24'>...</svg>")
UnsafeRaw takes a dynamic string and escapes nothing. Use it only with content you have already sanitized:
use proa_core::UnsafeRaw;
let sanitized = ammonia::clean(&user_html);
UnsafeRaw(&sanitized)
Never wrap request input, CMS strings, or user-authored Markdown in UnsafeRaw directly. Sanitize with an allowlist first, ammonia on the server, DOMPurify in the browser.
Good to know: The typed
AttrKeyenum deliberately excludes event-handler attributes such asonclick. Inline handlers cannot execute under a Content Security Policyscript-srcdirective, so the typed API does not encourage them. The macro still accepts them if you write them by hand.
Next steps
- HTML rendering
- Render typed Rust values to HTML with render traits and the html! macros.
- Security
- What Proa hardens by default, and what stays your job.
- Headers and CSP
- Set frame policy, HSTS, and a nonce-based Content Security Policy.
- Writing performant pages
- How to write zero-allocation Proa pages that render in microseconds. Covers format!() avoidance, SafeText, ClassList, numeric rendering, buffer selection, and pre-formatting patterns.