nak / mitra vibe fuzz

Last active 2 hours ago

Like 0

Findings from a vibe fuzzing session I can PR some fix attempts here too if you want, but wanted to at least pass the remaining 2 findings upstream

nak revised this gist 2 hours ago · d512e53

1 file changed, 93 insertions

fuzz_findings.md (file created)
@@ -0,0 +1,93 @@
1 + # Remaining fuzzing findings for Mitra
2 +
3 + Session date: 2026-08-02. Tested at `8b5c382b` (Mitra v5.8.0) with
4 + `cargo-fuzz`, libFuzzer, and AddressSanitizer.
5 +
6 + The previously reported `clean_title` byte/character mismatch is intentionally
7 + not included here. Its fix is being handled in PR #254. The two observations
8 + below remain; neither produced an escaping bypass or memory-safety issue in the
9 + tests performed.
10 +
11 + | # | Finding | Severity | Introduced | First released |
12 + | --- | --- | --- | --- | --- |
13 + | 1 | `clean_title` truncation splits HTML entities | Low | `7b2b46d5` | v2.14.0 |
14 + | 2 | `clean_remote_content` is not idempotent after one pass | Informational | dependency behaviour | n/a |
15 +
16 + ## 1. `clean_title` truncation splits HTML entities
17 +
18 + **Impact.** A truncated title can contain a partial HTML entity and render as
19 + corrupted text. Cleaning is not idempotent. No escaping bypass was observed;
20 + the fragment remains escaped.
21 +
22 + **Mechanism.** `clean_title` first calls `clean_html_all`, which escapes `<` as
23 + `&lt;`, and then truncates the resulting string by characters. The truncation
24 + can land inside an entity, leaving a suffix such as `&l`. A second cleaning pass
25 + escapes that bare `&` as `&amp;`, so the result changes again.
26 +
27 + Reproducer:
28 +
29 + ```rust
30 + use mitra_validators::posts::clean_title;
31 +
32 + let input = format!("aaa{}", "<".repeat(75));
33 + let once = clean_title(&input);
34 + let twice = clean_title(&once);
35 +
36 + assert_ne!(once, twice);
37 + ```
38 +
39 + Observed tails:
40 +
41 + ```text
42 + pass 1: ...lt;&lt;&l...
43 + pass 2: ...lt;&lt;&a...
44 + ```
45 +
46 + **History.** Commit `7b2b46d5` (2024-03-23, v2.14.0), "Remove HTML tags from
47 + post title", changed the first step from `title.trim()` to
48 + `clean_html_all(title).trim().to_owned()`. This placed escaped entities in the
49 + string that was already being truncated by character count.
50 +
51 + **Possible handling.** Ensure truncation cannot cut an `&...;` sequence, or
52 + truncate before HTML escaping in a way that preserves the sanitizer's output
53 + guarantees. The character-count change from PR #254 does not address this
54 + separate behavior.
55 +
56 + ## 2. `clean_remote_content` is not idempotent after one pass
57 +
58 + **Impact.** Informational. The observed input converges by the third pass and no
59 + dangerous markup survives. This is recorded because sanitizer output that
60 + re-parses into a different tree has the same general shape as mutation-XSS
61 + issues, even though no exploit was found here.
62 +
63 + **Mechanism.** An unknown tag leaves the HTML parser in a state where a
64 + following heading is re-nested on the next pass.
65 +
66 + Input:
67 +
68 + ```text
69 + v555<h4><hh4><h3>55\t
70 + ```
71 +
72 + Output over repeated calls to `clean_remote_content`:
73 +
74 + ```text
75 + pass 1: v555<h4><h3>55\t</h3></h4>
76 + pass 2: v555<h4></h4><h3>55\t</h3>
77 + pass 3: v555<h4></h4><h3>55\t</h3>
78 + ```
79 +
80 + `clean_remote_content` uses `clean_html`, which relies on ammonia's default tag
81 + allowlist. The `h3` and `h4` tags are not from Mitra's `CONTENT_ALLOWED_TAGS`;
82 + that list applies only to the strict local-content path.
83 +
84 + **History.** This was not traced to a Mitra defect. It is html5ever's reparse
85 + behavior for invalid heading nesting, exposed through ammonia. The tested
86 + versions were `ammonia 4.1.4` and `html5ever 0.39.0`.
87 + `clean_remote_content` itself dates to `d7750442` (2025-02-26, v3.18.0),
88 + "Refactor content cleaners", while the underlying parser behavior predates it.
89 +
90 + **Current fuzz-harness handling.** The `html_sanitize` target asserts
91 + convergence within eight passes and checks that no executable tags survive,
92 + rather than requiring one-pass idempotence. This keeps the target useful while
93 + retaining coverage of the known behavior.