fix(render): escape URLs in links and images to prevent XSS

Apply html_escape to:
- Link href and title attributes (start_tag_to_html)
- Image src attribute (Event::End TagEnd::Image handler)

Add test cases for:
- Quote-breaking URL attacks
- Link title escaping
- Image src escaping

Addresses HIGH severity finding from security audit.
All 70 tests pass.
This commit is contained in:
Timothy DeHerrera
2026-02-05 17:10:48 -07:00
parent e4a6305a50
commit 6638696dea

View File

@@ -149,13 +149,13 @@ pub fn markdown_to_html(markdown: &str) -> (String, Vec<Anchor>) {
if title.is_empty() {
html_output.push_str(&format!(
"<img src=\"{}\" alt=\"{}\" />",
src,
html_escape(&src),
html_escape(&alt)
));
} else {
html_output.push_str(&format!(
"<img src=\"{}\" alt=\"{}\" title=\"{}\" />",
src,
html_escape(&src),
html_escape(&alt),
html_escape(&title)
));
@@ -283,9 +283,13 @@ fn start_tag_to_html(tag: &Tag) -> String {
dest_url, title, ..
} => {
if title.is_empty() {
format!("<a href=\"{}\">", dest_url)
format!("<a href=\"{}\">", html_escape(&dest_url))
} else {
format!("<a href=\"{}\" title=\"{}\">", dest_url, title)
format!(
"<a href=\"{}\" title=\"{}\">",
html_escape(&dest_url),
html_escape(&title)
)
}
}
Tag::Image { .. } => String::new(), // Handled separately in main loop
@@ -459,4 +463,37 @@ Config details.
// Consecutive special chars → single hyphen
assert_eq!(slugify("A -- B"), "a-b");
}
#[test]
fn test_link_url_escaping() {
// Quote-breaking attack
let md = r#"[click]("><script>alert(1)</script>)"#;
let (html, _) = markdown_to_html(md);
assert!(!html.contains("<script>"), "script tags should be escaped");
assert!(html.contains("&gt;"), "angle brackets should be escaped");
// JavaScript URL (should be escaped, not executed)
let md = r#"[click](javascript:alert(1))"#;
let (html, _) = markdown_to_html(md);
assert!(html.contains("href=\"javascript:alert(1)\""));
}
#[test]
fn test_link_title_escaping() {
let md = r#"[text](url "title with \"quotes\"")"#;
let (html, _) = markdown_to_html(md);
assert!(html.contains("&quot;"), "quotes in title should be escaped");
}
#[test]
fn test_image_src_escaping() {
// Quote-breaking attack in image src
let md = r#"![alt]("><script>alert(1)</script>)"#;
let (html, _) = markdown_to_html(md);
assert!(!html.contains("<script>"), "script tags should be escaped");
assert!(
html.contains("&quot;") || html.contains("&gt;"),
"special chars in src should be escaped"
);
}
}