68 lines
2.1 KiB
Rust
68 lines
2.1 KiB
Rust
//! HTML sanitisation for user-supplied text fields.
|
|
//!
|
|
//! Uses ammonia to strip dangerous HTML tags and attributes while preserving
|
|
//! the visible text content. Sanitisation that actually fires is logged at
|
|
//! WARN so operators can spot abuse patterns.
|
|
|
|
use sha2::Digest;
|
|
use std::collections::HashSet;
|
|
|
|
/// Sanitise `value` for the named `field`.
|
|
///
|
|
/// Strips all HTML tags (keeping their text content) and removes dangerous
|
|
/// attributes. Returns `(sanitised_value, was_modified)`. When `was_modified`
|
|
/// is `true` the caller should log at WARN.
|
|
pub(super) fn sanitize_html(field: &str, value: &str) -> (String, bool) {
|
|
// Build an ammonia cleaner that allows NO tags but keeps text content.
|
|
// clear_content_tags is also set to empty so that <script>...</script>
|
|
// content is preserved as literal text rather than silently discarded.
|
|
let clean = ammonia::Builder::new()
|
|
.tags(HashSet::new())
|
|
.clean_content_tags(HashSet::new())
|
|
.clean(value)
|
|
.to_string();
|
|
|
|
let modified = clean != value;
|
|
if modified {
|
|
crate::slog_warn!(
|
|
"[validation] HTML sanitised in field '{}': fingerprint={}",
|
|
field,
|
|
fingerprint(value)
|
|
);
|
|
}
|
|
(clean, modified)
|
|
}
|
|
|
|
/// Return an 8-hex-char SHA-256 fingerprint of the input string.
|
|
fn fingerprint(input: &str) -> String {
|
|
let hash = sha2::Sha256::digest(input.as_bytes());
|
|
hash[..4].iter().map(|b| format!("{b:02x}")).collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn script_tags_stripped_content_preserved() {
|
|
let (out, modified) = sanitize_html("name", "<script>alert('xss')</script>");
|
|
assert!(modified);
|
|
assert!(!out.contains("<script>"));
|
|
assert!(out.contains("alert("));
|
|
}
|
|
|
|
#[test]
|
|
fn plain_text_unchanged() {
|
|
let (out, modified) = sanitize_html("name", "Hello World");
|
|
assert!(!modified);
|
|
assert_eq!(out, "Hello World");
|
|
}
|
|
|
|
#[test]
|
|
fn on_event_stripped() {
|
|
let (out, modified) = sanitize_html("name", r#"<img onload="evil()">"#);
|
|
assert!(modified);
|
|
assert!(!out.contains("onload"));
|
|
}
|
|
}
|