From e2eee6a7fce366321294c9c2a79f3146891dcbdf Mon Sep 17 00:00:00 2001 From: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> Date: Tue, 2 Jun 2026 03:35:02 +0800 Subject: [PATCH] templates: Patch for GHSA-vcc4-2c75-vc9v (#7785) * Patch GHSA-vcc4-2c75-vc9v in stripHTML templates: fix funcStripHTML bypass via depth counter The previous false-start approach allowed XSS bypass via inputs like <<>img src=x onerror=alert(1)> and failed on stacked angle brackets. Replace the tagStart/inTag state machine with a depth counter that mirrors PHP strip_tags behaviour: each '<' increments depth, each '>' decrements it, and text is only emitted at depth zero. Quoted attribute values (both single and double) are tracked so '>' inside href values does not prematurely close a tag. Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> * Update tplcontext_test.go Templates: expand TestStripHTML with attack path coverage Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> --------- Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com> --- modules/caddyhttp/templates/tplcontext.go | 47 +++++++++---------- .../caddyhttp/templates/tplcontext_test.go | 40 ++++++++++++++-- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index ee553e7a5..4e8ec925a 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -312,35 +312,32 @@ func (c TemplateContext) Host() (string, error) { return host, nil } -// funcStripHTML returns s without HTML tags. It is fairly naive -// but works with most valid HTML inputs. +// funcStripHTML returns s without HTML tags. Similar to PHP's strip_tags() func (TemplateContext) funcStripHTML(s string) string { var buf bytes.Buffer - var inTag, inQuotes bool - var tagStart int - for i, ch := range s { - if inTag { - if ch == '>' && !inQuotes { - inTag = false - } else if ch == '<' && !inQuotes { - // false start - buf.WriteString(s[tagStart:i]) - tagStart = i - } else if ch == '"' { - inQuotes = !inQuotes + depth := 0 + var quoteChar rune + for _, ch := range s { + switch { + case depth > 0 && quoteChar == 0 && (ch == '"' || ch == '\''): + // entering a quoted attribute value + quoteChar = ch + case depth > 0 && ch == quoteChar: + // leaving a quoted attribute value + quoteChar = 0 + case ch == '<' && quoteChar == 0: + depth++ + case ch == '>' && quoteChar == 0: + if depth > 0 { + depth-- + } else { + buf.WriteRune(ch) // stray '>' with no opening '<', keep it + } + default: + if depth == 0 { + buf.WriteRune(ch) } - continue } - if ch == '<' { - inTag = true - tagStart = i - continue - } - buf.WriteRune(ch) - } - if inTag { - // false start - buf.WriteString(s[tagStart:]) } return buf.String() } diff --git a/modules/caddyhttp/templates/tplcontext_test.go b/modules/caddyhttp/templates/tplcontext_test.go index 67ebbac70..1ff6caef0 100644 --- a/modules/caddyhttp/templates/tplcontext_test.go +++ b/modules/caddyhttp/templates/tplcontext_test.go @@ -419,14 +419,44 @@ func TestStripHTML(t *testing.T) { expect: `h1`, }, { - // tags not closed + // unclosed tag — trailing text must be stripped, not emitted input: `hi`, - expect: `' only closes one level + input: `hi`, + expect: ``, + }, + { + // XSS bypass via double opening bracket + input: `<<>img src=x onerror=alert('XSS')>`, + expect: ``, + }, + { + // stacked angle brackets (PHP strip_tags parity) + input: `<<<<<>>>>>hello`, + expect: `hello`, + }, + { + // unclosed tag strips trailing text + input: `hello ' inside double-quoted attribute must not close tag early + input: `text`, + expect: `text`, + }, + { + // '>' inside single-quoted attribute must not close tag early + input: `text`, + expect: `text`, + }, + { + // stray '>' with no opening '<' is preserved + input: `stray > bracket`, + expect: `stray > bracket`, }, } { actual := tplContext.funcStripHTML(test.input)