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>pull/7737/head^2 v2.11.4
parent
0e8eb41b87
commit
e2eee6a7fc
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,14 +419,44 @@ func TestStripHTML(t *testing.T) {
|
|||
expect: `h1`,
|
||||
},
|
||||
{
|
||||
// tags not closed
|
||||
// unclosed tag — trailing text must be stripped, not emitted
|
||||
input: `<h1`,
|
||||
expect: `<h1`,
|
||||
expect: ``,
|
||||
},
|
||||
{
|
||||
// false start
|
||||
input: `<h1<b>hi`,
|
||||
expect: `<h1hi`,
|
||||
// false start — second '<' increments depth, single '>' only closes one level
|
||||
input: `<h1<b>hi`,
|
||||
expect: ``,
|
||||
},
|
||||
{
|
||||
// XSS bypass via double opening bracket
|
||||
input: `<<>img src=x onerror=alert('XSS')>`,
|
||||
expect: ``,
|
||||
},
|
||||
{
|
||||
// stacked angle brackets (PHP strip_tags parity)
|
||||
input: `<<<<<>>>>><b>hello</b>`,
|
||||
expect: `hello`,
|
||||
},
|
||||
{
|
||||
// unclosed tag strips trailing text
|
||||
input: `hello <world`,
|
||||
expect: `hello `,
|
||||
},
|
||||
{
|
||||
// '>' inside double-quoted attribute must not close tag early
|
||||
input: `<a href="foo>bar">text</a>`,
|
||||
expect: `text`,
|
||||
},
|
||||
{
|
||||
// '>' inside single-quoted attribute must not close tag early
|
||||
input: `<a href='foo>bar'>text</a>`,
|
||||
expect: `text`,
|
||||
},
|
||||
{
|
||||
// stray '>' with no opening '<' is preserved
|
||||
input: `stray > bracket`,
|
||||
expect: `stray > bracket`,
|
||||
},
|
||||
} {
|
||||
actual := tplContext.funcStripHTML(test.input)
|
||||
|
|
|
|||
Loading…
Reference in New Issue