Skip to main content

max / makenotwork

custom-pages: make the HTML oracle observable cargo mutants (infra 897f0c85) found the oracle was not checked by anything. `replace oracle::assert_html_safe with ()` SURVIVED, along with 50 mutations of the tag and attribute parser inside it: every caller -- the seed replay, the regression replay, the fuzz target -- asserted only that the oracle does not panic, and a function that does nothing does not panic either. This is precisely what git-command's first mutation run found, and the reason the mutants pass is filed separately from the fuzzing: fuzzing can never find it, because it only ever feeds the oracle inputs the sanitizer handles correctly. Worth recording what the same run said about the code under test: src/html_sanitizer.rs had ZERO surviving mutants. The sanitizer was defended; the thing checking it was not. Seventeen tests call assert_html_safe directly with output the sanitizer would never emit -- forbidden elements, event handlers in quoted, unquoted and spaced-equals forms, javascript:/vbscript: schemes, a scheme hidden behind a character reference, off-platform hrefs and srcset candidates -- plus the positive cases that cost docengine's oracle two false starts: escaped markup inside an attribute value, and prose in a text node.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-26 12:49 UTC
Signed with PGP, not checked
Commit: 0043c360b13797a4fa9ea9cdbd4239363ce45831
Parent: 681d708
1 file changed, +156 insertions, -0 deletions
@@ -843,6 +843,162 @@
843 843 out
844 844 }
845 845
846 + #[cfg(test)]
847 + mod html_tests {
848 + //! The HTML oracle must not be vacuous.
849 + //!
850 + //! It was, and a `cargo mutants` run said so (infra `897f0c85`):
851 + //! `replace oracle::assert_html_safe with ()` SURVIVED, along with 50
852 + //! mutations of the tag and attribute parser inside it. Everything that
853 + //! called the oracle -- the seed replay, the regression replay, the
854 + //! fuzz target -- only ever asserted that it does not panic, and a
855 + //! function that does nothing does not panic either.
856 + //!
857 + //! That is the exact finding `git-command`'s first mutation run
858 + //! produced, and the reason infra `897f0c85` exists as its own task:
859 + //! fuzzing cannot find it, because fuzzing only ever feeds the oracle
860 + //! inputs the sanitizer handles correctly.
861 + //!
862 + //! Worth stating what the same run said about the code under test:
863 + //! `src/html_sanitizer.rs` had ZERO surviving mutants. The sanitizer
864 + //! was defended; the thing checking it was not.
865 + //!
866 + //! These call [`assert_html_safe`] directly with hand-written output
867 + //! the sanitizer would never emit, because that is the only way to
868 + //! observe an assertion that correct code never trips.
869 +
870 + use super::*;
871 +
872 + fn policy() -> UrlPolicy {
873 + UrlPolicy::new(
874 + "https://u.makenot.work/alice/proj",
875 + ["makenot.work".to_string(), "u.makenot.work".to_string()],
876 + )
877 + .unwrap()
878 + }
879 +
880 + fn check(clean: &str) {
881 + assert_html_safe(clean, "test", "test input", &policy());
882 + }
883 +
884 + #[test]
885 + fn accepts_what_the_sanitizer_actually_emits() {
886 + check("<p>hello</p>");
887 + check(r#"<a href="/local" rel="nofollow ugc">x</a>"#);
888 + check(r#"<img src="https://u.makenot.work/a.png" alt="a">"#);
889 + check(r#"<img srcset="/a.png 1x, /b.png 2x">"#);
890 + check(r##"<a href="#frag">x</a>"##);
891 + // Escaped markup inside an attribute VALUE is the sanitizer
892 + // working. The oracle skips quoted values by construction, and
893 + // these two cost docengine's oracle two false starts.
894 + check(r#"<p title="&lt;script&gt;alert(1)&lt;/script&gt;">x</p>"#);
895 + check(r#"<p title="javascript:alert(1)">x</p>"#);
896 + // Prose in a text node is text, not markup.
897 + check("<p>write javascript: in a sentence</p>");
898 + check("<p>onerror= is not an attribute here</p>");
899 + }
900 +
901 + #[test]
902 + #[should_panic(expected = "<script>")]
903 + fn catches_a_script_element() {
904 + check("<p>ok</p><script>alert(1)</script>");
905 + }
906 +
907 + #[test]
908 + #[should_panic(expected = "<script>")]
909 + fn catches_a_closing_script_element() {
910 + check("</script>");
911 + }
912 +
913 + #[test]
914 + #[should_panic(expected = "<iframe>")]
915 + fn catches_an_iframe() {
916 + check(r#"<iframe src="/x"></iframe>"#);
917 + }
918 +
919 + #[test]
920 + #[should_panic(expected = "<script>")]
921 + fn tag_names_are_matched_case_insensitively() {
922 + check("<ScRiPt>alert(1)</ScRiPt>");
923 + }
924 +
925 + #[test]
926 + #[should_panic(expected = "event handler")]
927 + fn catches_an_event_handler() {
928 + check(r#"<img src="/a.png" onerror="alert(1)">"#);
929 + }
930 +
931 + #[test]
932 + #[should_panic(expected = "event handler")]
933 + fn catches_an_event_handler_with_an_unquoted_value() {
934 + check("<img src=/a.png onerror=alert(1)>");
935 + }
936 +
937 + #[test]
938 + #[should_panic(expected = "event handler")]
939 + fn catches_an_event_handler_with_space_before_equals() {
940 + check(r#"<img src="/a.png" onerror = "alert(1)">"#);
941 + }
942 +
943 + #[test]
944 + #[should_panic(expected = "javascript:")]
945 + fn catches_a_javascript_href() {
946 + check(r#"<a href="javascript:alert(1)">x</a>"#);
947 + }
948 +
949 + #[test]
950 + #[should_panic(expected = "javascript:")]
951 + fn catches_a_javascript_href_single_quoted() {
952 + check("<a href='javascript:alert(1)'>x</a>");
953 + }
954 +
955 + #[test]
956 + #[should_panic(expected = "javascript:")]
957 + fn catches_a_scheme_hidden_behind_a_character_reference() {
958 + // A browser decodes before it resolves, so the oracle has to.
959 + // Without `decode_entities` this reads as harmless.
960 + check(r#"<a href="&#106;avascript:alert(1)">x</a>"#);
961 + }
962 +
963 + #[test]
964 + #[should_panic(expected = "vbscript:")]
965 + fn catches_a_vbscript_href() {
966 + check(r#"<a href="vbscript:msgbox(1)">x</a>"#);
967 + }
968 +
969 + #[test]
970 + #[should_panic(expected = "off-platform")]
971 + fn catches_an_off_platform_href() {
972 + check(r#"<a href="https://evil.example/x">x</a>"#);
973 + }
974 +
975 + #[test]
976 + #[should_panic(expected = "off-platform")]
977 + fn catches_an_off_platform_image() {
978 + check(r#"<img src="https://evil.example/px.png">"#);
979 + }
980 +
981 + #[test]
982 + #[should_panic(expected = "off-platform")]
983 + fn catches_one_bad_candidate_in_a_srcset() {
984 + // The whole attribute is untrustworthy if any candidate is: a
985 + // responsive image set cannot be partly on-platform.
986 + check(r#"<img srcset="/a.png 1x, https://evil.example/b.png 2x">"#);
987 + }
988 +
989 + #[test]
990 + #[should_panic(expected = "off-platform")]
991 + fn catches_a_protocol_relative_reference() {
992 + check(r#"<a href="//evil.example/x">x</a>"#);
993 + }
994 +
995 + #[test]
996 + #[should_panic(expected = "off-platform")]
997 + fn catches_an_unquoted_off_platform_value() {
998 + check("<a href=https://evil.example/x>x</a>");
999 + }
1000 + }
1001 +
846 1002 #[cfg(test)]
847 1003 mod tests {
848 1004 //! The oracle must not be vacuous.