# contextual-encoder
contextual output encoding for XSS defense and safe literal embedding,
inspired by the [OWASP Java Encoder](https://owasp.org/owasp-java-encoder/).
## disclaimer
contextual-encoder is an independent Rust crate for contextual output encoding.
its API and security model are inspired by the OWASP Java Encoder.
this project is not affiliated with, endorsed by, or maintained by the OWASP
Foundation.
## what this is
a zero-dependency Rust library that encodes untrusted strings for safe
embedding in web output contexts (HTML, JavaScript, CSS, URI, XML) and
source literal contexts (Rust). each function targets a specific
output context so that only the necessary characters are encoded.
## quick start
```rust
use contextual_encoder::for_html;
// pick the encoder for the sink; the input is always untrusted
let safe = for_html("<script>alert('xss')</script>");
assert_eq!(safe, "<script>alert('xss')</script>");
```
see [examples](#examples) for the other contexts, or run
`cargo run --example contexts`.
## what this is not
- **not a sanitizer.** encoding `<script>` as `<script>` makes it
display safely — it does not remove it. if you need to allow a subset of
HTML, use a dedicated sanitizer.
- **not a validator.** tag names, attribute names, event handler names, and
URL schemes must be validated separately. encoding cannot make arbitrary
names safe.
- **not a URL validator.** `for_uri_component` encodes a URI component, not
a full URL. to embed an untrusted URL, validate its scheme and structure
first, then encode for the final sink (for example, an HTML attribute).
## supported contexts
### HTML
| `for_html` | text content + quoted attributes | most conservative — safe default |
| `for_html_content` | text content only | does not encode quotes |
| `for_html_attribute` | quoted attributes only | does not encode `>` |
| `for_html_unquoted_attribute` | unquoted attribute values | most aggressive |
### XML
| `for_xml` | XML content + quoted attributes | alias for `for_html` |
| `for_xml_content` | XML content only | alias for `for_html_content` |
| `for_xml_attribute` | quoted XML attributes | alias for `for_html_attribute` |
| `for_xml_comment` | XML comment content | `--` → `-~`, trailing `-` → `~` |
| `for_cdata` | CDATA section content | splits `]]>`, and a trailing `]`, to prevent premature close |
### XML 1.1
| `for_xml11` | XML 1.1 content + quoted attributes | restricted chars → `&#xHH;` |
| `for_xml11_content` | XML 1.1 content only | does not encode quotes |
| `for_xml11_attribute` | quoted XML 1.1 attributes | does not encode `>` |
### JavaScript
| `for_javascript` | general JS string contexts | caller supplies quotes; hex-encodes quotes for HTML safety |
| `for_javascript_attribute` | HTML event attributes | does not escape `<` or `/` |
| `for_javascript_block` | `<script>` blocks | uses backslash quote escapes |
| `for_javascript_source` | standalone .js files | minimal encoding; not valid JSON — use `for_json` |
| `for_js_template` | ES6 template literal content | escapes `` ` ``, `${`, and a trailing `$` |
the `<script>`-block encoders (`for_javascript`, `for_javascript_block`,
`for_js_template`) escape `<` as `\x3c` as well as `/` as `\/`. escaping `/`
alone stops a literal `</script>`, but not `<!--<script>`, which walks the
HTML tokenizer into script-data-double-escaped state — where the block's real
`</script>` no longer closes it.
they also escape `&` as `\x26`. in an HTML5 document `<script>` content is raw
text and character references are not decoded, so this matters only when the
page is parsed as XML (`application/xhtml+xml`, XSLT output) — there the parser
resolves them before JavaScript sees the text, so untrusted input containing
the literal ``` would arrive as a backtick that closes a template literal,
and `${` as a `${` that opens an interpolation.
all five escape the bidi formatting controls (U+202A-U+202E, U+2066-U+2069)
as `\uHHHH`. a direction override left raw in the generated JavaScript reorders
how the surrounding source reads, hiding code from review while the engine runs
it unchanged — the same Trojan Source hazard the Rust literal encoders close.
### CSS
| `for_css_string` | quoted CSS string values | hex escapes with separator spaces, including at the end; escapes the bidi formatting controls |
| `for_css_url` | CSS `url()` values, quoted or unquoted | like `for_css_string` plus space — output is always one url-token |
### URI
| `for_uri_component` | query params, path segments | RFC 3986 percent-encoding |
| `for_uri_path` | URI paths (preserves `/`) | RFC 3986 section 3.3 |
| `for_form_urlencoded` | form field values | WHATWG URL Standard, space → `+` |
### additional literal contexts
these encoders are not part of the OWASP Java Encoder's scope. they encode
untrusted strings for safe embedding in source code literals.
#### JSON
| `for_json` | JSON string values | `\uHHHH` escapes (no `\xHH`), escapes `<`, `/` and U+2028/U+2029 for `<script>` safety |
#### Rust
| `for_rust_string` | Rust string literals (`"..."`) | `\xHH` for controls, `\u{HHHH}` for the bidi controls `rustc` rejects raw, other non-ASCII passes through |
| `for_rust_char` | Rust char literals (`'...'`) | escapes `'` instead of `"`; input must be exactly one character |
| `for_rust_char_checked` | Rust char literals (`'...'`) | as `for_rust_char`, but returns `None` unless the input is exactly one character |
| `for_rust_byte_string` | Rust byte string literals (`b"..."`) | non-ASCII → `\xHH` per UTF-8 byte |
#### SQL
| `for_sql` | standard SQL string literals (`'...'`) | doubles `'` to `''`, removes NUL; backslash passes through |
| `for_sql_backslash` | MySQL/MariaDB string literals (`'...'`) | C-style backslash escaping (`\'`, `\\`, `\n`, etc.) |
## unsupported / dangerous contexts
the following contexts are **intentionally not supported** because encoding
cannot make them safe:
- **raw tag names** — validate against a whitelist
- **raw attribute names** — validate against a whitelist
- **event handler names** — validate against a whitelist
- **raw JavaScript expressions** — no encoder can make `eval()` safe
- **raw CSS selectors / properties** — validate structure separately
- **dynamic SQL identifiers (table / column names)** — `for_sql` encodes a
string literal (`'...'`), not an identifier (`"..."` or `` `...` ``);
validate against a whitelist
- **HTML comments** — vendor-specific extensions (e.g., IE conditional
comments) make safe encoding impractical
- **full untrusted URLs** — `for_uri_component` encodes a component, not a
full URL. to embed an untrusted URL, validate its scheme and structure
first, then encode for the final sink
## examples
```rust
use contextual_encoder::{for_html, for_javascript, for_css_string, for_uri_component};
let user_input = "<script>alert('xss')</script>";
// HTML text content or quoted attribute
let safe = for_html(user_input);
assert_eq!(safe, "<script>alert('xss')</script>");
// JavaScript string literal
let safe = for_javascript(user_input);
// quotes are hex-encoded, < and / are escaped to keep </script> closing
assert!(safe.contains(r"\x3c\/script>"));
// CSS string value
let safe = for_css_string(user_input);
assert!(safe.contains(r"\3c"));
// URI component
let safe = for_uri_component(user_input);
assert!(safe.contains("%3C"));
```
### writer-based API
every `for_*` function has a `write_*` counterpart that writes to any
`std::fmt::Write` implementor:
```rust
use contextual_encoder::write_html;
let mut buf = String::new();
write_html(&mut buf, "safe & sound").unwrap();
assert_eq!(buf, "safe & sound");
```
## security model
this is a **contextual output encoder**, not a sanitizer. it prevents
cross-site scripting by encoding output for specific contexts.
### caveats
**grave accent (`` ` ``):** unpatched internet explorer treats the grave
accent as an attribute delimiter. `for_html_unquoted_attribute` encodes it
as ```, but numeric character references decode back to the original
character, so this is not a complete fix. the safest mitigation is to avoid
unquoted attributes entirely.
**template literals:** the string literal JavaScript encoders (`for_javascript`,
`for_javascript_attribute`, etc.) do **not** encode backticks. to embed
untrusted data directly inside an ES6 template literal, use `for_js_template`:
```js
// WRONG — vulnerable:
// `Hello ${unsafeInput}`
// RIGHT — use the template literal encoder:
`Hello ${for_js_template(unsafeInput)}`
// ALSO RIGHT — encode into a variable first:
var x = '<contextual_encoder::for_javascript output>';
`Hello ${x}`
```
**full URLs:** `for_uri_component` encodes a URI component, not a full URL.
to embed an untrusted URL, validate its scheme and structure first, then
encode for the final sink (for example, an HTML attribute).
**HTML comments:** no HTML comment encoder is provided. HTML comments have
vendor-specific extensions (e.g., `<!--[if IE]>`) that make safe encoding
impractical. never embed untrusted data in HTML comments. `for_xml_comment`
is for XML comments only — it is **not safe for HTML comments**.
## relationship to OWASP Java Encoder
the web output encoders (HTML, JavaScript, CSS, URI, XML) are modeled on
the OWASP Java Encoder. the Rust literal encoders are additions
specific to this crate.
### exact matches
- encoding rules for `for_html`, `for_html_content`, `for_html_attribute`,
`for_html_unquoted_attribute`
- JavaScript encoding rules across all four contexts
- CSS hex escape format with trailing space separator (including C1 controls)
- URI component percent-encoding of UTF-8 bytes
- security caveats (grave accent, template literals, HTML comments, full URLs)
### intentional deviations
- **surrogate handling:** Java's `char[]` can contain invalid surrogate pairs
which the Java encoder replaces with space or dash. Rust `str` is guaranteed
valid UTF-8, so surrogates cannot appear. supplementary plane characters
(U+10000+) are valid and pass through or are encoded normally.
- **`for_html` uses `"` and `'`** for quote encoding rather than
`"` — both are valid HTML and the numeric form is shorter.
- **`-` (hyphen) in JavaScript:** the Java encoder may escape `-` as `\-` in
some JavaScript contexts to prevent `-->` sequences. this crate does not
encode `-` in JavaScript. the `-->` sequence inside a JS string literal is
harmless because the HTML parser does not scan string literal contents.
## license
MIT