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
+}