From 9154f6fe3c54d63e36d7dac274966d34ed60ccb7 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Thu, 2 Jul 2026 11:31:54 +0400 Subject: [PATCH] fix(deps): replace earmark with mdex --- changelog.d/earmark-mdex.security | 1 + lib/pleroma/formatter.ex | 43 ++++++++++++++++++++-- mix.exs | 3 +- mix.lock | 6 ++- test/pleroma/web/common_api/utils_test.exs | 9 +++++ 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 changelog.d/earmark-mdex.security diff --git a/changelog.d/earmark-mdex.security b/changelog.d/earmark-mdex.security new file mode 100644 index 000000000..b03da28ab --- /dev/null +++ b/changelog.d/earmark-mdex.security @@ -0,0 +1 @@ +Replace retired Earmark markdown rendering with MDEx to address stored XSS in link attributes. diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index 4bf2f6b95..a0f477a16 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -124,14 +124,49 @@ defmodule Pleroma.Formatter do end def markdown_to_html(text) do - Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false}) + markdown_to_html(text, %{}) end def markdown_to_html(text, opts) do - Earmark.as_html!( - text, - %Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts) + breaks = Map.get(opts, :breaks, false) + text = normalize_markdown_quirks(text) + + text + |> MDEx.to_html!( + extension: [autolink: true, strikethrough: true], + parse: [smart: false], + render: [hardbreaks: breaks, unsafe: true] ) + |> compact_markdown_html(text) + end + + defp compact_markdown_html(html, markdown) do + html + |> String.trim_trailing("\n") + |> String.replace(~r/>\n\s*<") + |> String.replace(~r/\n<\/code><\/pre>/, "") + |> String.replace(~r/(?)/, ~s()) + |> String.replace("
", "
") + |> unwrap_raw_html_paragraph(markdown) + end + + defp normalize_markdown_quirks(text) do + String.replace(text, ~r/~~([^~\n]+)~~~/, "~~\\1~~\\\\~") + end + + defp unwrap_raw_html_paragraph(html, markdown) do + if raw_html_only?(markdown) do + String.replace(html, ~r/\A

(.*)<\/p>\z/s, "\\1") + else + html + end + end + + defp raw_html_only?(markdown) do + markdown = String.trim(markdown) + + String.match?(markdown, ~r/\A(?: