From 11c78a7a771d4af505efeb754a0b8775689c2eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Fri, 20 Feb 2026 23:41:41 -0700 Subject: [PATCH] fix: escape single quotes in html_filter and HTML.escape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single quotes were not escaped by the HTML filter or HTML plugin's escape method, creating XSS risk in single-quoted HTML attributes. Uses ' (numeric entity) which is valid across all HTML versions, unlike ' which is only defined in XML. The xml_filter already handled single quotes via ' — its comment is updated to clarify the distinction. Test coverage added for both filter.t, html.t and vmethods/text.t. Co-Authored-By: Kōan --- lib/Template/Filters.pm | 10 ++++++---- lib/Template/Plugin/HTML.pm | 1 + t/filter.t | 8 +++++++- t/html.t | 7 +++++++ t/vmethods/text.t | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/Template/Filters.pm b/lib/Template/Filters.pm index deeb2f201..f033378e8 100644 --- a/lib/Template/Filters.pm +++ b/lib/Template/Filters.pm @@ -297,8 +297,9 @@ sub url_filter { #------------------------------------------------------------------------ # html_filter() [% FILTER html %] # -# Convert any '<', '>' or '&' characters to the HTML equivalents, '<', -# '>' and '&', respectively. +# Convert any '<', '>', '&', '"' or "'" characters to the HTML +# equivalents, '<', '>', '&', '"' and ''', +# respectively. #------------------------------------------------------------------------ sub html_filter { @@ -308,6 +309,7 @@ sub html_filter { s//>/g; s/"/"/g; + s/'/'/g; } return $text; } @@ -316,8 +318,8 @@ sub html_filter { #------------------------------------------------------------------------ # xml_filter() [% FILTER xml %] # -# Same as the html filter, but adds the conversion of ' to ' which -# is native to XML. +# Same as the html filter, but uses ' for single quotes (the XML +# named entity) instead of ' (the numeric reference used for HTML). #------------------------------------------------------------------------ sub xml_filter { diff --git a/lib/Template/Plugin/HTML.pm b/lib/Template/Plugin/HTML.pm index 3275093a5..58d11e3b8 100644 --- a/lib/Template/Plugin/HTML.pm +++ b/lib/Template/Plugin/HTML.pm @@ -115,6 +115,7 @@ sub escape { s//>/g; s/"/"/g; + s/'/'/g; } $text; } diff --git a/t/filter.t b/t/filter.t index 1e8f17bb9..e1aa2b4c8 100644 --- a/t/filter.t +++ b/t/filter.t @@ -291,7 +291,13 @@ The <cat> sat on the <mat> "It isn't what I expected", he replied. [% END %] -- expect -- -"It isn't what I expected", he replied. +"It isn't what I expected", he replied. + +-- test -- +-- name html filter single-quoted attributes -- +[% val = "it's & \"broken\""; val FILTER html %] +-- expect -- +it's <dangerous> & "broken" -- test -- [% FILTER xml %] diff --git a/t/html.t b/t/html.t index 1cc692af1..8479dffcd 100644 --- a/t/html.t +++ b/t/html.t @@ -104,6 +104,13 @@ my%20file.html -- expect -- if (a < b && c > d) ... +-- test -- +-- name escape single quotes -- +[% USE HTML -%] +[% HTML.escape("it's a test") %] +-- expect -- +it's a <tag attr='val'>test + -- test -- -- name sorted -- [% USE HTML(sorted=1) -%] diff --git a/t/vmethods/text.t b/t/vmethods/text.t index 13e6b36bd..a9ec5e21a 100644 --- a/t/vmethods/text.t +++ b/t/vmethods/text.t @@ -215,7 +215,7 @@ Tim O'Reilly said \"Oh really?\" -- name text.html -- [% markup.html %] -- expect -- -a < b > & c "d" 'e' +a < b > & c "d" 'e' -- test -- -- name text.xml --