@marcel91200 / caddy Mirror · read-only / commits / e2eee6a7
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 09:45 UTC

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>

JM Sanchez committed Jun 2, 2026 at 03:35 UTC e2eee6a7fce366321294c9c2a79f3146891dcbdf
2 files changed +57 -30
modules/caddyhttp/templates/tplcontext.go
+22 -25
@@ -312,35 +312,32 @@ func (c TemplateContext) Host() (string, error) {
312 return host, nil
313 }
314
315 -// funcStripHTML returns s without HTML tags. It is fairly naive
316 -// but works with most valid HTML inputs.
315 +// funcStripHTML returns s without HTML tags. Similar to PHP's strip_tags()
316 func (TemplateContext) funcStripHTML(s string) string {
317 var buf bytes.Buffer
319 - var inTag, inQuotes bool
320 - var tagStart int
321 - for i, ch := range s {
322 - if inTag {
323 - if ch == '>' && !inQuotes {
324 - inTag = false
325 - } else if ch == '<' && !inQuotes {
326 - // false start
327 - buf.WriteString(s[tagStart:i])
328 - tagStart = i
329 - } else if ch == '"' {
330 - inQuotes = !inQuotes
318 + depth := 0
319 + var quoteChar rune
320 + for _, ch := range s {
321 + switch {
322 + case depth > 0 && quoteChar == 0 && (ch == '"' || ch == '\''):
323 + // entering a quoted attribute value
324 + quoteChar = ch
325 + case depth > 0 && ch == quoteChar:
326 + // leaving a quoted attribute value
327 + quoteChar = 0
328 + case ch == '<' && quoteChar == 0:
329 + depth++
330 + case ch == '>' && quoteChar == 0:
331 + if depth > 0 {
332 + depth--
333 + } else {
334 + buf.WriteRune(ch) // stray '>' with no opening '<', keep it
335 + }
336 + default:
337 + if depth == 0 {
338 + buf.WriteRune(ch)
339 }
332 - continue
333 - }
334 - if ch == '<' {
335 - inTag = true
336 - tagStart = i
337 - continue
340 }
339 - buf.WriteRune(ch)
340 - }
341 - if inTag {
342 - // false start
343 - buf.WriteString(s[tagStart:])
341 }
342 return buf.String()
343 }
modules/caddyhttp/templates/tplcontext_test.go
+35 -5
@@ -419,14 +419,44 @@ func TestStripHTML(t *testing.T) {
419 expect: `h1`,
420 },
421 {
422 - // tags not closed
422 + // unclosed tag — trailing text must be stripped, not emitted
423 input: `<h1`,
424 - expect: `<h1`,
424 + expect: ``,
425 },
426 {
427 - // false start
428 - input: `<h1<b>hi`,
429 - expect: `<h1hi`,
427 + // false start — second '<' increments depth, single '>' only closes one level
428 + input: `<h1<b>hi`,
429 + expect: ``,
430 + },
431 + {
432 + // XSS bypass via double opening bracket
433 + input: `<<>img src=x onerror=alert('XSS')>`,
434 + expect: ``,
435 + },
436 + {
437 + // stacked angle brackets (PHP strip_tags parity)
438 + input: `<<<<<>>>>><b>hello</b>`,
439 + expect: `hello`,
440 + },
441 + {
442 + // unclosed tag strips trailing text
443 + input: `hello <world`,
444 + expect: `hello `,
445 + },
446 + {
447 + // '>' inside double-quoted attribute must not close tag early
448 + input: `<a href="foo>bar">text</a>`,
449 + expect: `text`,
450 + },
451 + {
452 + // '>' inside single-quoted attribute must not close tag early
453 + input: `<a href='foo>bar'>text</a>`,
454 + expect: `text`,
455 + },
456 + {
457 + // stray '>' with no opening '<' is preserved
458 + input: `stray > bracket`,
459 + expect: `stray > bracket`,
460 },
461 } {
462 actual := tplContext.funcStripHTML(test.input)