Skip to content

fix(letters): encode the mailto address; fuzz the register check and the letters weekly - #29

Merged
EquityBot merged 4 commits into
mainfrom
ryan/fuzz-targets-2026-11-02
Nov 2, 2026
Merged

EquityBot merged 4 commits into
mainfrom
ryan/fuzz-targets-2026-11-02

Conversation

@EquityBot

Copy link
Copy Markdown
Owner

Fixes a mailto injection in the letters, and adds fuzz targets for the register check and the letters.

The fix. letters::mailto wrote the register entry's address into the link unencoded. The register check accepts any address RFC 5322 allows, including ?, &, # and % in the local part, so an entry whose address is x?cc=attacker%40evil.example%2Cy@broker.example.com.au passes registry check and its link opened a draft with a cc to attacker@evil.example: the user's request, with their identity details, would also go to the attacker. A link is only made for a letter short enough for one (2,000 characters): none of the 161 full letters the 80 shipped entries compose fits, but 165 single-lever letters do, so the injection is reachable. Each part of the address around its @ is now percent-encoded like the subject and body. The new test a_register_address_cannot_add_fields_to_a_mailto_link uses that address; it fails on main. No release contains the bug (GhostSoap has none yet); it came with mailto in #3.

Fuzzing.

  • fuzz/: register_letters runs an entry file through the register check and, when it passes, composes its letters, checks the eml header block and decodes each mailto link's address, subject and body to check they are exactly the letter's, for the letter and for a short copy of it; letter_text runs the renderer and the line wrapper on any text and width. Own workspace, with a lockfile seeded from the shipped Cargo.lock so the targets fuzz the dependency versions that ship (it adds only arbitrary, jobserver and libfuzzer-sys).
  • fuzz.yml: every Monday and on demand, 5 minutes per target on nightly with cargo-fuzz 0.13.2, seeded from registry/au and letters/au. A crashing input is kept as a workflow artefact.
  • Dependabot also watches fuzz/Cargo.lock, and CONTRIBUTING says how to run a target.

Checked on Linux on this branch merged with main as of #27: cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --locked (104 passed; 102 on Windows before #27), the release build and registry check --path registry/au (80 entries, 0 errors). Each target ran for 180 seconds without a crash: letter_text 2,146,088 inputs and register_letters 1,596,439. A throwaway harness ran the target's decoding check over every shipped entry: all 80 pass and their 64 short-copy links decode exactly, and it rejects the old unencoded link for an added entry whose address is x?subject=spoof@acceleon.com.au.

Alcuin approved it with no findings.

The register check accepts any address RFC 5322 allows, so a register entry's
address may hold '?', '&', '#' or '%'. mailto() wrote it unencoded, so an entry
such as x?cc=attacker%40evil.example%2Cy@broker.example.com.au ended the
address early and added a cc field: the user's request, with their personal
details, would also go to the attacker. Each part of the address around its
'@' is now percent-encoded like the subject and body.
register_letters runs an entry file through the register check and, when it
passes, composes its letters and checks the eml header block and the mailto
fields. letter_text runs the renderer and the line wrapper on any text and
width. CI fuzzes each for 5 minutes every Monday, seeded from registry/au and
letters/au; Dependabot covers the fuzz crate's lockfile.
Review found that the link check let an address such as
x?subject=spoof@broker.example.com.au through on the old construction, since
it only counted fields. The fuzz target now decodes the link's address,
subject and body and compares them with the letter's. A whole request is
usually over the 2,000-character link limit (none of the 161 letters the 81
shipped entries compose gets a link), so the target also checks the link of a
short copy of each letter.
The fuzz crate's lock was generated fresh, so it held newer versions of
crates the parent lock pins. Seeding it from Cargo.lock means the targets
fuzz the dependency versions that ship; it adds only the fuzzing crates.
@sonarqubecloud

sonarqubecloud Bot commented Nov 2, 2026

Copy link
Copy Markdown

@EquityBot
EquityBot enabled auto-merge (squash) November 2, 2026 12:34
@codacy-production

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 medium

Alerts:
⚠ 1 issue (≤ 0 issues of at least minor severity)

Results:
1 new issue

Category Results
Complexity 1 medium

View in Codacy

🟢 Metrics 11 complexity · 0 duplication

Metric Results
Complexity 11
Duplication 0

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

⚠️ Resolve issues before merging

Two issues warrant attention before merging. The fuzz dependency set is not clearly reproducibly pinned for clean workflow runs, and the address encoding logic may alter valid addresses containing an '@' within a quoted local part. No uncovered complex files were reported.

Test suggestions

  • An address containing query, fragment, percent, or ampersand characters is safely encoded without adding mailto fields.
  • The mailto address, subject, and body round-trip through percent decoding to their original values.
  • The existing 2,000-character mailto limit remains enforced.
  • Register fuzzing parses valid inputs, composes letters, validates EML headers, and checks mailto decoding.
  • Letter fuzzing exercises rendering and wrapping across arbitrary text and widths.
  • The fuzz workflow runs both targets weekly and on demand and uploads crash artifacts.
  • The fuzz crate has a committed lockfile so locked dependency versions are reproducible.
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. The fuzz crate has a committed lockfile so locked dependency versions are reproducible.

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread fuzz/Cargo.toml
Comment on lines +1 to +3
[package]
name = "ghostsoap-fuzz"
version = "0.0.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM RISK

Commit the fuzz crate's Cargo.lock and ensure the workflow uses it so fuzz dependencies are reproducibly pinned.

TIP React 👍 if this review is right, or reply with what's wrong and how it could be better to help improve the reviewer.

@EquityBot
EquityBot merged commit e5678ef into main Nov 2, 2026
13 of 14 checks passed
@EquityBot
EquityBot deleted the ryan/fuzz-targets-2026-11-02 branch November 2, 2026 12:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant