5c0d11b1
feat(server): tighten readme sanitizer img URL schemes
alex emery 2026-04-12 18:36
diff --git a/src/server/http/repo/readme.rs b/src/server/http/repo/readme.rs index ec16449..729345e 100644 --- a/src/server/http/repo/readme.rs +++ b/src/server/http/repo/readme.rs @@ -117,8 +117,20 @@ fn html_escape(s: &str) -> String { } fn sanitize(html: &str) -> String { // Task 5 will tighten the policy (img URL schemes). Default is fine for now. ammonia::clean(html) use std::collections::HashSet; // Restrict <img src> URL schemes to http/https only — no data:, no javascript:. // ammonia::Builder::url_schemes() applies the allowlist to ALL URL-bearing // attributes, which is what we want. let mut schemes: HashSet<&str> = HashSet::new(); schemes.insert("http"); schemes.insert("https"); schemes.insert("mailto"); ammonia::Builder::default() .url_schemes(schemes) .clean(html) .to_string() } #[cfg(test)] @@ -263,4 +275,50 @@ mod tests { assert!(load_readme(&repo).is_none()); } #[test] fn markdown_strips_script_tag() { let (repo, _tmp) = repo_with_files(&[ ("README.md", b"# t\n\n<script>alert(1)</script>\n"), ]); let html = load_readme(&repo).unwrap().html; assert!(!html.contains("<script>"), "got: {}", html); } #[test] fn markdown_strips_javascript_href() { let (repo, _tmp) = repo_with_files(&[ ("README.md", b"[click](javascript:alert(1))\n"), ]); let html = load_readme(&repo).unwrap().html; assert!(!html.contains("javascript:"), "got: {}", html); } #[test] fn markdown_strips_onerror_attribute() { let (repo, _tmp) = repo_with_files(&[ ("README.md", b"<img src=\"https://x/y.png\" onerror=\"alert(1)\">\n"), ]); let html = load_readme(&repo).unwrap().html; assert!(!html.contains("onerror"), "got: {}", html); } #[test] fn markdown_strips_iframe() { let (repo, _tmp) = repo_with_files(&[ ("README.md", b"<iframe src=\"https://evil.example/\"></iframe>\n"), ]); let html = load_readme(&repo).unwrap().html; assert!(!html.contains("<iframe"), "got: {}", html); } #[test] fn markdown_strips_data_image_uri() { let (repo, _tmp) = repo_with_files(&[ ("README.md", b"<img src=\"data:image/png;base64,AAAA\">\n"), ]); let html = load_readme(&repo).unwrap().html; // Either the whole <img> is dropped or the src attr is gone. assert!(!html.contains("data:"), "got: {}", html); } }