@marcel91200 / caddy Mirror · read-only / commits / 719db53e
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, 02:15 UTC

caddyfile: check heredoc markers without a regexp (#8249)

The formatter checked every token starting with "<<" against the heredoc marker regexp. In a libFuzzer build each string comparison copies both strings, and the regexp compares the remaining input on every step, so a long marker-shaped token made the fuzz-format target time out. A byte loop accepts the same markers in linear time. The lexer uses it too, so there is one definition of a valid marker.

awss committed Oct 17, 2026 at 14:22 UTC 719db53e946181dd10a9aae36721c3251f7f8dc1
3 files changed +52 -4
caddyconfig/caddyfile/formatter.go
+1 -1
@@ -410,7 +410,7 @@ func hasUnformattableToken(tokens []Token) bool {
410 continue
411 }
412 marker := raw[2:]
413 - if !heredocMarkerRegexp.MatchString(marker) {
413 + if !isHeredocMarker(marker) {
414 continue
415 }
416 if i+1 >= len(tokens) || fmtNextOnNewLine(tk, tokens[i+1]) {
caddyconfig/caddyfile/lexer.go
+18 -3
@@ -19,7 +19,6 @@ import (
19 "bytes"
20 "fmt"
21 "io"
22 - "regexp"
22 "strings"
23 "unicode"
24 )
@@ -255,7 +254,7 @@ func (l *lexer) next() (bool, error) {
254 }
255
256 heredocMarker = string(val[2:])
258 - if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
257 + if !isHeredocMarker(heredocMarker) {
258 return false, fmt.Errorf("heredoc marker on line #%d must contain only alphanumeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
259 }
260
@@ -612,7 +611,23 @@ func (t Token) Clone() Token {
611 }
612 }
613
615 -var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
614 +// isHeredocMarker reports whether marker is a valid heredoc marker, made of
615 +// one or more ASCII letters, digits, dashes and underscores. It scans bytes
616 +// instead of matching a regexp because regexp matching of a long string is
617 +// quadratic under libFuzzer instrumentation.
618 +func isHeredocMarker(marker string) bool {
619 + if len(marker) == 0 {
620 + return false
621 + }
622 + for i := range len(marker) {
623 + c := marker[i]
624 + isAlnum := 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9'
625 + if !isAlnum && c != '_' && c != '-' {
626 + return false
627 + }
628 + }
629 + return true
630 +}
631
632 // isNextOnNewLine tests whether t2 is on a different line from t1
633 func isNextOnNewLine(t1, t2 Token) bool {
caddyconfig/caddyfile/lexer_test.go
+33
@@ -814,3 +814,36 @@ func BenchmarkLexerLongHeredoc(b *testing.B) {
814 }
815 }
816 }
817 +
818 +// TestIsHeredocMarker covers the marker check shared by the lexer and the
819 +// formatter, including a long marker like the one that made the formatter time
820 +// out under libFuzzer when the check was a regexp.
821 +func TestIsHeredocMarker(t *testing.T) {
822 + long := strings.Repeat("a", 500000)
823 + for i, tc := range []struct {
824 + marker string
825 + want bool
826 + }{
827 + {marker: "END", want: true},
828 + {marker: "a-b_C9", want: true},
829 + {marker: "09AZaz", want: true},
830 + {marker: long, want: true},
831 + {marker: "", want: false},
832 + {marker: "EN D", want: false},
833 + {marker: "END\n", want: false},
834 + {marker: "<END", want: false},
835 + {marker: "/", want: false},
836 + {marker: ":", want: false},
837 + {marker: "@", want: false},
838 + {marker: "[", want: false},
839 + {marker: "`", want: false},
840 + {marker: "{", want: false},
841 + {marker: "ÉND", want: false},
842 + {marker: "END\xff", want: false},
843 + {marker: long + "<<<<", want: false},
844 + } {
845 + if got := isHeredocMarker(tc.marker); got != tc.want {
846 + t.Errorf("Test %d: isHeredocMarker(%.20q) = %v, want %v", i, tc.marker, got, tc.want)
847 + }
848 + }
849 +}