diff --git a/lib/custom_markdown_renderer.rb b/lib/custom_markdown_renderer.rb index e7eefe5c8..d5c9a3351 100644 --- a/lib/custom_markdown_renderer.rb +++ b/lib/custom_markdown_renderer.rb @@ -77,9 +77,10 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer return nil unless embed_config template = embed_config['template'] - # Use Ruby's built-in named captures with gsub to handle CSS % values + # Use gsub (not format) so CSS `%` values in templates don't need escaping. + # Captured values are HTML-escaped since they land inside HTML attribute contexts. match_data.named_captures.each do |var_name, value| - template = template.gsub("%{#{var_name}}", value) + template = template.gsub("%{#{var_name}}", CGI.escapeHTML(value)) end template end diff --git a/spec/lib/custom_markdown_renderer_spec.rb b/spec/lib/custom_markdown_renderer_spec.rb index 3415a811d..cb13f6cf9 100644 --- a/spec/lib/custom_markdown_renderer_spec.rb +++ b/spec/lib/custom_markdown_renderer_spec.rb @@ -238,5 +238,23 @@ describe CustomMarkdownRenderer do expect(output).to include('allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"') end end + + context 'when captured values contain HTML-special characters' do + # CommonMark angle-bracket link destinations `[text]()` permit characters + # like `"` that the embed regex captures would otherwise pass through raw into + # attribute values. Captures are HTML-escaped before interpolation so the + # substituted value cannot break out of the surrounding attribute context. + it 'escapes double quotes in captured YouTube video_id' do + markdown = "\n[demo]()\n" + output = render_markdown(markdown) + expect(output).not_to include('onload="alert(1)"') + expect(output).to include('"') + end + + it 'leaves legitimate alphanumeric IDs untouched' do + output = render_markdown_link('https://www.youtube.com/watch?v=dQw4w9WgXcQ') + expect(output).to include('src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"') + end + end end end