pub(crate) fn is_blocked_tag(name: &str) -> bool {
matches!(
name,
"script"
| "iframe"
| "object"
| "embed"
| "noscript"
| "head"
| "meta"
| "link"
| "title"
| "base"
| "template"
| "frame"
| "frameset"
| "style"
)
}
pub(crate) fn is_blocked_attr(name: &str) -> bool {
if name.starts_with("on") {
return true;
}
matches!(name, "srcdoc" | "formaction")
}
pub(crate) fn is_safe_url(url: &str) -> bool {
let trimmed = url.trim();
if trimmed.is_empty() {
return false;
}
if !contains_scheme(trimmed) {
return true;
}
let lower = trimmed.to_ascii_lowercase();
if lower.starts_with("http://")
|| lower.starts_with("https://")
|| lower.starts_with("mailto:")
|| lower.starts_with("tel:")
{
return true;
}
if let Some(rest) = lower.strip_prefix("data:") {
return rest.starts_with("image/") && !rest.starts_with("image/svg");
}
false
}
fn contains_scheme(url: &str) -> bool {
let bytes = url.as_bytes();
if bytes.is_empty() || !bytes[0].is_ascii_alphabetic() {
return false;
}
for (i, &b) in bytes.iter().enumerate().skip(1) {
match b {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'+' | b'.' | b'-' => continue,
b':' => return i > 0,
_ => return false,
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn data_svg_urls_are_rejected() {
assert!(is_safe_url("data:image/png;base64,AAAA"));
assert!(!is_safe_url("data:image/svg+xml,<svg onload=alert(1)/>"));
assert!(!is_safe_url("DATA:image/SVG+xml;base64,AAAA"));
}
#[test]
fn allowed_urls_pass() {
for url in [
"https://damascene.dev",
"http://example.com/path",
"mailto:user@example.com",
"tel:+15551234",
"/relative/path",
"./file.html",
"../sibling",
"#anchor",
"?query=1",
"data:image/png;base64,AAA",
] {
assert!(is_safe_url(url), "expected safe: {url}");
}
}
#[test]
fn dangerous_urls_blocked() {
for url in [
"javascript:alert(1)",
"JAVASCRIPT:alert(1)",
"vbscript:msgbox",
"data:text/html,<script>",
"data:application/javascript,alert(1)",
"",
" ",
] {
assert!(!is_safe_url(url), "expected blocked: {url}");
}
}
#[test]
fn on_attrs_blocked() {
assert!(is_blocked_attr("onclick"));
assert!(is_blocked_attr("onerror"));
assert!(is_blocked_attr("onmouseover"));
assert!(!is_blocked_attr("href"));
assert!(!is_blocked_attr("class"));
}
#[test]
fn script_and_iframe_blocked() {
assert!(is_blocked_tag("script"));
assert!(is_blocked_tag("iframe"));
assert!(is_blocked_tag("object"));
assert!(!is_blocked_tag("div"));
assert!(!is_blocked_tag("p"));
}
}