Skip to main content

max / multithreaded

Add ammonia HTML sanitizer as second pass after pulldown-cmark Closes the last cold spot (Frontend B+). Markdown output now goes through ammonia::clean() before storage, providing defense-in-depth against any pulldown-cmark parsing bugs that might let raw HTML through. Same pattern used by GO and BB. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-03-14 18:50 UTC
Commit: e69bb8c8105913c2251707097881f569739a3101
Parent: c2c5747
3 files changed, +230 insertions, -2 deletions
M Cargo.lock +225
@@ -18,6 +18,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
18 18 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
19 19
20 20 [[package]]
21 + name = "ammonia"
22 + version = "4.1.2"
23 + source = "registry+https://github.com/rust-lang/crates.io-index"
24 + checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
25 + dependencies = [
26 + "cssparser",
27 + "html5ever",
28 + "maplit",
29 + "tendril",
30 + "url",
31 + ]
32 +
33 + [[package]]
21 34 name = "android_system_properties"
22 35 version = "0.1.5"
23 36 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -350,6 +363,29 @@ dependencies = [
350 363 ]
351 364
352 365 [[package]]
366 + name = "cssparser"
367 + version = "0.35.0"
368 + source = "registry+https://github.com/rust-lang/crates.io-index"
369 + checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa"
370 + dependencies = [
371 + "cssparser-macros",
372 + "dtoa-short",
373 + "itoa",
374 + "phf",
375 + "smallvec",
376 + ]
377 +
378 + [[package]]
379 + name = "cssparser-macros"
380 + version = "0.6.1"
381 + source = "registry+https://github.com/rust-lang/crates.io-index"
382 + checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
383 + dependencies = [
384 + "quote",
385 + "syn",
386 + ]
387 +
388 + [[package]]
353 389 name = "dashmap"
354 390 version = "6.1.0"
355 391 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -420,6 +456,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
420 456 checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
421 457
422 458 [[package]]
459 + name = "dtoa"
460 + version = "1.0.11"
461 + source = "registry+https://github.com/rust-lang/crates.io-index"
462 + checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
463 +
464 + [[package]]
465 + name = "dtoa-short"
466 + version = "0.3.5"
467 + source = "registry+https://github.com/rust-lang/crates.io-index"
468 + checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
469 + dependencies = [
470 + "dtoa",
471 + ]
472 +
473 + [[package]]
423 474 name = "either"
424 475 version = "1.15.0"
425 476 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -545,6 +596,16 @@ dependencies = [
545 596 ]
546 597
547 598 [[package]]
599 + name = "futf"
600 + version = "0.1.5"
601 + source = "registry+https://github.com/rust-lang/crates.io-index"
602 + checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
603 + dependencies = [
604 + "mac",
605 + "new_debug_unreachable",
606 + ]
607 +
608 + [[package]]
548 609 name = "futures"
549 610 version = "0.3.32"
550 611 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -818,6 +879,17 @@ dependencies = [
818 879 ]
819 880
820 881 [[package]]
882 + name = "html5ever"
883 + version = "0.35.0"
884 + source = "registry+https://github.com/rust-lang/crates.io-index"
885 + checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4"
886 + dependencies = [
887 + "log",
888 + "markup5ever",
889 + "match_token",
890 + ]
891 +
892 + [[package]]
821 893 name = "http"
822 894 version = "1.4.0"
823 895 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1202,6 +1274,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1202 1274 checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
1203 1275
1204 1276 [[package]]
1277 + name = "mac"
1278 + version = "0.1.1"
1279 + source = "registry+https://github.com/rust-lang/crates.io-index"
1280 + checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
1281 +
1282 + [[package]]
1283 + name = "maplit"
1284 + version = "1.0.2"
1285 + source = "registry+https://github.com/rust-lang/crates.io-index"
1286 + checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
1287 +
1288 + [[package]]
1289 + name = "markup5ever"
1290 + version = "0.35.0"
1291 + source = "registry+https://github.com/rust-lang/crates.io-index"
1292 + checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3"
1293 + dependencies = [
1294 + "log",
1295 + "tendril",
1296 + "web_atoms",
1297 + ]
1298 +
1299 + [[package]]
1300 + name = "match_token"
1301 + version = "0.35.0"
1302 + source = "registry+https://github.com/rust-lang/crates.io-index"
1303 + checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf"
1304 + dependencies = [
1305 + "proc-macro2",
1306 + "quote",
1307 + "syn",
1308 + ]
1309 +
1310 + [[package]]
1205 1311 name = "matchers"
1206 1312 version = "0.2.0"
1207 1313 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1280,6 +1386,7 @@ dependencies = [
1280 1386 name = "multithreaded"
1281 1387 version = "0.2.0"
1282 1388 dependencies = [
1389 + "ammonia",
1283 1390 "askama",
1284 1391 "axum",
1285 1392 "base64",
@@ -1328,6 +1435,12 @@ dependencies = [
1328 1435 ]
1329 1436
1330 1437 [[package]]
1438 + name = "new_debug_unreachable"
1439 + version = "1.0.6"
1440 + source = "registry+https://github.com/rust-lang/crates.io-index"
1441 + checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
1442 +
1443 + [[package]]
1331 1444 name = "no-std-compat"
1332 1445 version = "0.4.1"
1333 1446 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1501,6 +1614,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1501 1614 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1502 1615
1503 1616 [[package]]
1617 + name = "phf"
1618 + version = "0.11.3"
1619 + source = "registry+https://github.com/rust-lang/crates.io-index"
1620 + checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
1621 + dependencies = [
1622 + "phf_macros",
1623 + "phf_shared",
1624 + ]
1625 +
1626 + [[package]]
1627 + name = "phf_codegen"
1628 + version = "0.11.3"
1629 + source = "registry+https://github.com/rust-lang/crates.io-index"
1630 + checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
1631 + dependencies = [
1632 + "phf_generator",
1633 + "phf_shared",
1634 + ]
1635 +
1636 + [[package]]
1637 + name = "phf_generator"
1638 + version = "0.11.3"
1639 + source = "registry+https://github.com/rust-lang/crates.io-index"
1640 + checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
1641 + dependencies = [
1642 + "phf_shared",
1643 + "rand 0.8.5",
1644 + ]
1645 +
1646 + [[package]]
1647 + name = "phf_macros"
1648 + version = "0.11.3"
1649 + source = "registry+https://github.com/rust-lang/crates.io-index"
1650 + checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
1651 + dependencies = [
1652 + "phf_generator",
1653 + "phf_shared",
1654 + "proc-macro2",
1655 + "quote",
1656 + "syn",
1657 + ]
1658 +
1659 + [[package]]
1660 + name = "phf_shared"
1661 + version = "0.11.3"
1662 + source = "registry+https://github.com/rust-lang/crates.io-index"
1663 + checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
1664 + dependencies = [
1665 + "siphasher",
1666 + ]
1667 +
1668 + [[package]]
1504 1669 name = "pin-project"
1505 1670 version = "1.1.11"
1506 1671 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1596,6 +1761,12 @@ dependencies = [
1596 1761 ]
1597 1762
1598 1763 [[package]]
1764 + name = "precomputed-hash"
1765 + version = "0.1.1"
1766 + source = "registry+https://github.com/rust-lang/crates.io-index"
1767 + checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
1768 +
1769 + [[package]]
1599 1770 name = "prettyplease"
1600 1771 version = "0.2.37"
1601 1772 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2097,6 +2268,12 @@ dependencies = [
2097 2268 ]
2098 2269
2099 2270 [[package]]
2271 + name = "siphasher"
2272 + version = "1.0.2"
2273 + source = "registry+https://github.com/rust-lang/crates.io-index"
2274 + checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
2275 +
2276 + [[package]]
2100 2277 name = "slab"
2101 2278 version = "0.4.12"
2102 2279 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2356,6 +2533,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2356 2533 checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
2357 2534
2358 2535 [[package]]
2536 + name = "string_cache"
2537 + version = "0.8.9"
2538 + source = "registry+https://github.com/rust-lang/crates.io-index"
2539 + checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
2540 + dependencies = [
2541 + "new_debug_unreachable",
2542 + "parking_lot",
2543 + "phf_shared",
2544 + "precomputed-hash",
2545 + "serde",
2546 + ]
2547 +
2548 + [[package]]
2549 + name = "string_cache_codegen"
2550 + version = "0.5.4"
2551 + source = "registry+https://github.com/rust-lang/crates.io-index"
2552 + checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
2553 + dependencies = [
2554 + "phf_generator",
2555 + "phf_shared",
2556 + "proc-macro2",
2557 + "quote",
2558 + ]
2559 +
2560 + [[package]]
2359 2561 name = "stringprep"
2360 2562 version = "0.1.5"
2361 2563 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2438,6 +2640,17 @@ dependencies = [
2438 2640 ]
2439 2641
2440 2642 [[package]]
2643 + name = "tendril"
2644 + version = "0.4.3"
2645 + source = "registry+https://github.com/rust-lang/crates.io-index"
2646 + checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
2647 + dependencies = [
2648 + "futf",
2649 + "mac",
2650 + "utf-8",
2651 + ]
2652 +
2653 + [[package]]
2441 2654 name = "thiserror"
2442 2655 version = "1.0.69"
2443 2656 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3133,6 +3346,18 @@ dependencies = [
3133 3346 ]
3134 3347
3135 3348 [[package]]
3349 + name = "web_atoms"
3350 + version = "0.1.3"
3351 + source = "registry+https://github.com/rust-lang/crates.io-index"
3352 + checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414"
3353 + dependencies = [
3354 + "phf",
3355 + "phf_codegen",
3356 + "string_cache",
3357 + "string_cache_codegen",
3358 + ]
3359 +
3360 + [[package]]
3136 3361 name = "whoami"
3137 3362 version = "1.6.1"
3138 3363 source = "registry+https://github.com/rust-lang/crates.io-index"
M Cargo.toml +2
@@ -44,6 +44,7 @@ governor = "0.8"
44 44 chrono = { version = "0.4", features = ["serde"] }
45 45 uuid = { version = "1", features = ["v4", "serde"] }
46 46 pulldown-cmark = "0.12"
47 + ammonia = "4"
47 48 askama = "0.13"
48 49
49 50 # Internal crates
@@ -77,6 +78,7 @@ sha2 = { workspace = true }
77 78 base64 = { workspace = true }
78 79 rand = { workspace = true }
79 80 pulldown-cmark = { workspace = true }
81 + ammonia = { workspace = true }
80 82 tower_governor = { workspace = true }
81 83 governor = { workspace = true }
82 84 dotenvy = "0.15"
M src/markdown.rs +3 -2
@@ -53,7 +53,7 @@ pub fn render(input: &str) -> String {
53 53 });
54 54 let mut output = String::new();
55 55 html::push_html(&mut output, parser);
56 - output
56 + ammonia::clean(&output)
57 57 }
58 58
59 59 #[cfg(test)]
@@ -93,7 +93,8 @@ mod tests {
93 93 #[test]
94 94 fn link_renders() {
95 95 let result = render("[example](https://example.com)");
96 - assert!(result.contains(r#"<a href="https://example.com">example</a>"#));
96 + assert!(result.contains(r#"href="https://example.com""#));
97 + assert!(result.contains("example</a>"));
97 98 }
98 99
99 100 #[test]