Skip to content

[2.x] fix: stop email values being parsed as markup - #4874

Merged
imorland merged 1 commit into
2.xfrom
im/mail-safe-substitution
Jul 31, 2026
Merged

[2.x] fix: stop email values being parsed as markup#4874
imorland merged 1 commit into
2.xfrom
im/mail-safe-substitution

Conversation

@imorland

Copy link
Copy Markdown
Member

The problem

Notification email bodies are translation strings containing markup, with user-supplied values as the link text:

{poster_display_name} just posted in a discussion you're following: [{title}]({url}).

The parameters are substituted before the string is handed to the formatter, so the parser sees the values. With the markdown extension enabled — bundled, and on most forums — a discussion title of:

Innocent](https://evil.example.com)[rest

closes the intended link and opens its own, producing a notification, sent by the forum with its correct headers and styling, whose visible link goes wherever the discussion's author chose:

<a href="https://evil.example.com">Innocent</a><a href="…/d/100">rest</a>

An image reference in a title (![](https://evil.example.com/beacon.png)) instead turns the email into a tracking beacon that fires on open, disclosing the recipient's address and client to a third party. 80 characters is ample for either.

The surface is wider than the title: the same templates interpolate display names ({poster_display_name}, {mentioner_display_name}, {replier_display_name}, {user_display_name}), which are often self-service, across subscriptions, mentions and messages. Even without the markdown extension, core's own autolinker turns a bare URL in a title into a clickable link in the email.

The fix

Email views are given a translator that replaces parameter values with opaque markers, and a formatter that puts the values back — HTML-escaped — once rendering is done. The parser never sees user data.

  • No template changes. Swapping the pair in a view composer (MailServiceProvider) rather than fixing nine call sites matters because most affected templates live in extensions, and many will never be updated. Third-party notification emails are fixed by upgrading core.
  • Stateless. The value travels inside its own marker rather than in shared state, so templates that render repeatedly — core's notification view calls convert() three times, and extension views render a preview beside the body — cannot mix values between renders.
  • Markers are hex. Base64's +/= are altered by markdown parsing and URL encoding; letters and digits survive both, so {url} in a link destination still round-trips.
  • Escaping was rejected as the fix. Escaping markdown metacharacters in the value visibly corrupts ordinary titles (Version 2\.0 \- what\'s new\!) and, for one payload, destroys the notification link entirely — a user-visible regression on every forum, and hand-rolled CommonMark escaping is a future bug in its own right.

Tests

  • framework/core — 8 tests covering what core alone guarantees: a value can never be autolinked, embed an image, redirect a link or inject HTML, while the trusted template still renders and ordinary titles are not littered.
  • extensions/subscriptions — 6 tests with markdown enabled, using the real body template, covering the full exploit. flarum/markdown added to require-dev for this.

Both suites were written first and observed failing on the real attack. Verified live on a forum with markdown enabled: both payloads render as inert text inside the correct link.

Regression: core mail/notification (20), forum/frontend (49), subscriptions (21), mentions (82), messages (12), suspend (19), PHPStan — all pass.

Note on the test suite

The subscriptions test inlines the body template rather than translating its key, because integration tests do not register Extend\Locales translations (#4600) — trans() would return the key and leave nothing to attack. There is a @todo to switch back when that is fixed in 2.1.

Credit

Reported by Arpit Jain (@arpitjain099), who verified it by reading the source and was explicit about not having run it. No advisory or CVE is issued, per the policy of not treating pre-stable releases as covered — 2.x is still RC.

Email bodies are translation strings containing markup — a markdown link
whose text is a discussion title, say — rendered by the formatter after
their parameters have been substituted in. That order puts user values in
front of the parser: a title containing ](...) closes the intended link
and opens its own, so a notification sent by the forum points wherever
its author likes, and an image reference turns the mail into a beacon
that fires when it is opened. Display names reach the same templates, and
are often self-service.

Email views now get a translator that replaces parameter values with
opaque markers, and a formatter that puts the values back, escaped, once
rendering is done. The parser never sees them. Marking happens per call
with the value carried inside the marker, so templates that render
several times — core's notification view converts three times, and
extension views render a preview alongside the body — cannot mix values
between renders.

Swapping the pair for email views rather than fixing each template means
no template changes, which matters because the templates at risk live
mostly in extensions, many of which will never be updated.

Reported by Arpit Jain (@arpitjain099).
@imorland
imorland requested a review from a team as a code owner July 31, 2026 23:31
@imorland imorland added this to the 2.0.0-rc.6 milestone Jul 31, 2026
@imorland
imorland merged commit 063e067 into 2.x Jul 31, 2026
25 checks passed
@imorland
imorland deleted the im/mail-safe-substitution branch July 31, 2026 23:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant