Skip to main content

xss_filters/
lib.rs

1//! Context-aware output encoders that prevent cross-site scripting.
2//!
3//! Each filter encodes only the characters needed to keep an untrusted string
4//! from breaking out of a specific HTML output context. The supported contexts
5//! are HTML data, HTML comments, single/double/unquoted attribute values, and
6//! URIs placed inside any of those. URI filters normalize IPv6 hosts and block
7//! dangerous protocols such as `javascript:` and `data:`.
8//!
9//! The design encodes the minimal set of characters required to thwart a
10//! context break, rather than entity-escaping everything. This is faster and
11//! avoids double-encoding.
12//!
13//! # Choosing a filter
14//!
15//! Match the filter to the exact place the value lands in the document.
16//!
17//! ```
18//! use xss_filters::{in_html_data, in_double_quoted_attr, uri_in_double_quoted_attr};
19//!
20//! // <div>{{ value }}</div>
21//! assert_eq!(in_html_data("<script>"), "&lt;script>");
22//!
23//! // <input value="{{ value }}">
24//! assert_eq!(in_double_quoted_attr("a\"b"), "a&quot;b");
25//!
26//! // <a href="{{ value }}">
27//! assert_eq!(
28//!     uri_in_double_quoted_attr("javascript:alert(1)"),
29//!     "x-javascript:alert(1)"
30//! );
31//! ```
32//!
33//! # Input contract
34//!
35//! All public functions take `&str` and return `String`. JavaScript-style
36//! coercion of non-string values is the caller's job. Apply these filters only
37//! to UTF-8 documents. Do not apply them inside scriptable contexts such as
38//! `<script>`, `<style>`, `<object>`, `<embed>`, `<svg>`, or inside `style` and
39//! `on*` attributes.
40//!
41//! # URI encoding and surrogates
42//!
43//! The URI filters build on encoders equivalent to JavaScript `encodeURI` and
44//! `encodeURIComponent`. Those throw on a lone UTF-16 surrogate. A Rust `&str`
45//! is always well-formed UTF-8, so the `&str` entry points cannot reach that
46//! case. [`uri::encode_uri_units`] reproduces the throw for raw UTF-16 input.
47
48#![cfg_attr(not(feature = "std"), no_std)]
49#![forbid(unsafe_code)]
50#![warn(missing_docs)]
51
52extern crate alloc;
53use alloc::string::String;
54
55#[cfg(test)]
56extern crate std;
57
58mod core;
59pub mod uri;
60
61#[cfg(any(feature = "css", test))]
62pub mod css;
63
64// ---------------------------------------------------------------------------
65// Generic context filters
66// ---------------------------------------------------------------------------
67
68/// Encode `<` as `&lt;` for HTML data context.
69///
70/// Use inside element content such as `<div>{{ s }}</div>`. Only `<` is
71/// encoded. `>`, `&`, `"`, and `'` pass through.
72///
73/// ```
74/// assert_eq!(xss_filters::in_html_data("foo&<>'\""), "foo&&lt;>'\"");
75/// ```
76pub fn in_html_data(s: &str) -> String {
77    core::yd(s)
78}
79
80/// Neutralize HTML comment closers for HTML comment context.
81///
82/// Use inside `<!-- {{ s }} -->`. Sequences that could close the comment
83/// (`-->`, `--!>`, a leading `-*>`), collaborate to close it (a trailing `--!`,
84/// `--`, or `-`), or trigger IE conditional comments (`]>` or a trailing `]`)
85/// gain a space. NUL becomes the replacement character.
86///
87/// ```
88/// assert_eq!(xss_filters::in_html_comment("foo-->bar"), "foo-- >bar");
89/// ```
90pub fn in_html_comment(s: &str) -> String {
91    core::yc(s)
92}
93
94/// Encode `'` as `&#39;` for single-quoted attribute context.
95///
96/// Use inside `<input value='{{ s }}'>`. Not for `on*` attributes. For URIs use
97/// the matching `uri*_in_single_quoted_attr` filter.
98///
99/// ```
100/// assert_eq!(xss_filters::in_single_quoted_attr("it's"), "it&#39;s");
101/// ```
102pub fn in_single_quoted_attr(s: &str) -> String {
103    core::yavs(s)
104}
105
106/// Encode `"` as `&quot;` for double-quoted attribute context.
107///
108/// Use inside `<input value="{{ s }}">`. Not for `on*` attributes. For URIs use
109/// the matching `uri*_in_double_quoted_attr` filter.
110///
111/// ```
112/// assert_eq!(xss_filters::in_double_quoted_attr("a\"b"), "a&quot;b");
113/// ```
114pub fn in_double_quoted_attr(s: &str) -> String {
115    core::yavd(s)
116}
117
118/// Encode attribute-breaking characters for unquoted attribute context.
119///
120/// Use inside `<input value={{ s }}>`. Whitespace, quotes, backtick, `=`, `<`,
121/// `>`, and NUL are encoded. An empty input yields a single replacement
122/// character so the value cannot silently swallow the next attribute.
123///
124/// ```
125/// assert_eq!(xss_filters::in_unquoted_attr(""), "\u{FFFD}");
126/// assert_eq!(xss_filters::in_unquoted_attr("a b"), "a&#32;b");
127/// ```
128pub fn in_unquoted_attr(s: &str) -> String {
129    core::yavu(s)
130}
131
132// ---------------------------------------------------------------------------
133// URI matrix: helpers
134// ---------------------------------------------------------------------------
135
136/// Compose a full or path URI attribute filter: encode, attr-escape, then guard.
137fn uri_in_attr(s: &str, attr: fn(&str) -> String, encode: fn(&str) -> String) -> String {
138    core::yubl(&attr(&encode(s)))
139}
140
141// ---------------------------------------------------------------------------
142// Full URI (encodeURI + IPv6 restore, protocol guard on attrs)
143// ---------------------------------------------------------------------------
144
145/// Full URI inside a single-quoted attribute.
146///
147/// `encodeURI` plus IPv6 restore, then single-quote escape, then protocol guard.
148pub fn uri_in_single_quoted_attr(s: &str) -> String {
149    uri_in_attr(s, core::yavs, core::yufull)
150}
151
152/// Full URI inside a double-quoted attribute.
153pub fn uri_in_double_quoted_attr(s: &str) -> String {
154    uri_in_attr(s, core::yavd, core::yufull)
155}
156
157/// Full URI inside an unquoted attribute.
158pub fn uri_in_unquoted_attr(s: &str) -> String {
159    uri_in_attr(s, core::yavu, core::yufull)
160}
161
162/// Full URI in HTML data context. Only the URI encoder runs, since `<` is
163/// already `%3C`.
164pub fn uri_in_html_data(s: &str) -> String {
165    core::yufull(s)
166}
167
168/// Full URI in HTML comment context.
169pub fn uri_in_html_comment(s: &str) -> String {
170    core::yc(&core::yufull(s))
171}
172
173// ---------------------------------------------------------------------------
174// URI path / query (encodeURI, no IPv6 restore, protocol guard on attrs)
175// ---------------------------------------------------------------------------
176
177/// URI path or query inside a single-quoted attribute.
178pub fn uri_path_in_single_quoted_attr(s: &str) -> String {
179    uri_in_attr(s, core::yavs, core::yu)
180}
181
182/// URI path or query inside a double-quoted attribute.
183pub fn uri_path_in_double_quoted_attr(s: &str) -> String {
184    uri_in_attr(s, core::yavd, core::yu)
185}
186
187/// URI path or query inside an unquoted attribute.
188pub fn uri_path_in_unquoted_attr(s: &str) -> String {
189    uri_in_attr(s, core::yavu, core::yu)
190}
191
192/// URI path or query in HTML data context.
193pub fn uri_path_in_html_data(s: &str) -> String {
194    core::yu(s)
195}
196
197/// URI path or query in HTML comment context.
198pub fn uri_path_in_html_comment(s: &str) -> String {
199    core::yc(&core::yu(s))
200}
201
202// URI query filters are exact aliases of the path filters.
203
204/// URI query inside a single-quoted attribute. Alias of the path filter.
205pub fn uri_query_in_single_quoted_attr(s: &str) -> String {
206    uri_path_in_single_quoted_attr(s)
207}
208
209/// URI query inside a double-quoted attribute. Alias of the path filter.
210pub fn uri_query_in_double_quoted_attr(s: &str) -> String {
211    uri_path_in_double_quoted_attr(s)
212}
213
214/// URI query inside an unquoted attribute. Alias of the path filter.
215pub fn uri_query_in_unquoted_attr(s: &str) -> String {
216    uri_path_in_unquoted_attr(s)
217}
218
219/// URI query in HTML data context. Alias of the path filter.
220pub fn uri_query_in_html_data(s: &str) -> String {
221    uri_path_in_html_data(s)
222}
223
224/// URI query in HTML comment context. Alias of the path filter.
225pub fn uri_query_in_html_comment(s: &str) -> String {
226    uri_path_in_html_comment(s)
227}
228
229// ---------------------------------------------------------------------------
230// URI component (encodeURIComponent, no protocol guard)
231// ---------------------------------------------------------------------------
232
233/// URI component inside a single-quoted attribute.
234///
235/// No protocol guard runs because `encodeURIComponent` already percent-encodes
236/// the `:` that a `javascript:` scheme needs.
237pub fn uri_component_in_single_quoted_attr(s: &str) -> String {
238    core::yavs(&core::yuc(s))
239}
240
241/// URI component inside a double-quoted attribute.
242pub fn uri_component_in_double_quoted_attr(s: &str) -> String {
243    core::yavd(&core::yuc(s))
244}
245
246/// URI component inside an unquoted attribute.
247pub fn uri_component_in_unquoted_attr(s: &str) -> String {
248    core::yavu(&core::yuc(s))
249}
250
251/// URI component in HTML data context.
252pub fn uri_component_in_html_data(s: &str) -> String {
253    core::yuc(s)
254}
255
256/// URI component in HTML comment context.
257pub fn uri_component_in_html_comment(s: &str) -> String {
258    core::yc(&core::yuc(s))
259}
260
261// ---------------------------------------------------------------------------
262// URI fragment (encodeURIComponent; protocol guard on attrs)
263// ---------------------------------------------------------------------------
264
265/// URI fragment inside a single-quoted attribute.
266///
267/// Like the component filter but with the protocol guard, since a fragment
268/// pasted into `href` could carry `javascript:`.
269pub fn uri_fragment_in_single_quoted_attr(s: &str) -> String {
270    core::yubl(&core::yavs(&core::yuc(s)))
271}
272
273/// URI fragment inside a double-quoted attribute.
274pub fn uri_fragment_in_double_quoted_attr(s: &str) -> String {
275    core::yubl(&core::yavd(&core::yuc(s)))
276}
277
278/// URI fragment inside an unquoted attribute.
279pub fn uri_fragment_in_unquoted_attr(s: &str) -> String {
280    core::yubl(&core::yavu(&core::yuc(s)))
281}
282
283/// URI fragment in HTML data context. Alias of the component filter.
284pub fn uri_fragment_in_html_data(s: &str) -> String {
285    uri_component_in_html_data(s)
286}
287
288/// URI fragment in HTML comment context. Alias of the component filter.
289pub fn uri_fragment_in_html_comment(s: &str) -> String {
290    uri_component_in_html_comment(s)
291}