From 8b4aac7b270a4a20e45bd82b4e834f444492980f Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 29 Sep 2026 12:04:57 +0100 Subject: [PATCH 1/2] Escape image titles and alt text in Markdown A double quote in an image's title or alt text ended the HTML attribute early, so the title was cut short and the rest of the text leaked into the tag's markup. Escape the URL, title and alt text before they go into the link and image tags. --- lib/markdown_renderer.rb | 1 + test/lib/markdown_renderer_test.rb | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/lib/markdown_renderer.rb b/lib/markdown_renderer.rb index 97c49685..903067d0 100644 --- a/lib/markdown_renderer.rb +++ b/lib/markdown_renderer.rb @@ -20,6 +20,7 @@ def header(text, header_level) end def image(url, title, alt_text) + url, title, alt_text = [ url, title, alt_text ].map { ERB::Util.html_escape(it) } %(#{alt_text}) end diff --git a/test/lib/markdown_renderer_test.rb b/test/lib/markdown_renderer_test.rb index c3ad2119..348054ea 100644 --- a/test/lib/markdown_renderer_test.rb +++ b/test/lib/markdown_renderer_test.rb @@ -8,4 +8,13 @@ class MarkdownRendererTest < ActiveSupport::TestCase assert_includes content, "id='duplicated-header'" assert_includes content, "id='duplicated-header-2'" end + + test "it escapes image attributes" do + markdown = MarkdownRenderer.build + content = markdown.render(%(![a "b"](/image.png "x" data-x="y"))) + + assert_includes content, %(title="x" data-x="y") + assert_includes content, %(alt="a "b"") + assert_empty Nokogiri::HTML5.fragment(content).css("[data-x]") + end end From 824ba3d25e25df92b404ad6a4301a4ada3aec98d Mon Sep 17 00:00:00 2001 From: Donal McBreen Date: Tue, 29 Sep 2026 12:11:19 +0100 Subject: [PATCH 2/2] Keep existing entities in image titles and alt text, and test URL escaping --- lib/markdown_renderer.rb | 2 +- test/lib/markdown_renderer_test.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/markdown_renderer.rb b/lib/markdown_renderer.rb index 903067d0..042b3a2c 100644 --- a/lib/markdown_renderer.rb +++ b/lib/markdown_renderer.rb @@ -20,7 +20,7 @@ def header(text, header_level) end def image(url, title, alt_text) - url, title, alt_text = [ url, title, alt_text ].map { ERB::Util.html_escape(it) } + url, title, alt_text = [ url, title, alt_text ].map { ERB::Util.html_escape_once(it) } %(#{alt_text}) end diff --git a/test/lib/markdown_renderer_test.rb b/test/lib/markdown_renderer_test.rb index 348054ea..ca5315cc 100644 --- a/test/lib/markdown_renderer_test.rb +++ b/test/lib/markdown_renderer_test.rb @@ -11,10 +11,11 @@ class MarkdownRendererTest < ActiveSupport::TestCase test "it escapes image attributes" do markdown = MarkdownRenderer.build - content = markdown.render(%(![a "b"](/image.png "x" data-x="y"))) + content = markdown.render(%(![a "b" & c](/image.png?a=1&b=2 "x" data-x="y"))) assert_includes content, %(title="x" data-x="y") - assert_includes content, %(alt="a "b"") + assert_includes content, %(alt="a "b" & c") + assert_includes content, %(href="/image.png?a=1&b=2") assert_empty Nokogiri::HTML5.fragment(content).css("[data-x]") end end