templates: Patch for GHSA-vcc4-2c75-vc9v (#7785)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m48s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m47s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m14s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m58s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m5s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m46s
Lint / govulncheck (push) Successful in 1m33s
Lint / dependency-review (push) Failing after 1m2s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m54s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 5m49s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled

* 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>
This commit is contained in:
JM Sanchez
2026-06-02 03:35:02 +08:00
committed by GitHub
parent 0e8eb41b87
commit e2eee6a7fc
2 changed files with 57 additions and 30 deletions
+22 -25
View File
@@ -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()
}
+35 -5
View File
@@ -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)