From 2192af80f405c70e3021191ff8b3f7e539c35e9d Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Tue, 5 May 2026 17:46:21 +0530 Subject: [PATCH] fix: html-escape captured values in helpcenter article markdown embeds (#14140) Embed templates interpolate regex captures from user-authored article URLs into HTML attribute values. CommonMark's angle-bracket link destination syntax allows characters that the capture regexes don't filter, so the unescaped substitution could produce malformed attribute output. Escaping at substitution time keeps the render deterministic regardless of the URL. ### How was this tested? Added specs. Fixes [CW-6934](https://linear.app/chatwoot/issue/CW-6934/) Co-authored-by: Sony Mathew Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- lib/custom_markdown_renderer.rb | 5 +++-- spec/lib/custom_markdown_renderer_spec.rb | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) 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