Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 10, 23:45 UTC
caddyfile: reject site blocks with unbalanced braces (#8375)
A braced site block whose last directive has its own block was accepted with its closing brace missing, because the directive's closing brace satisfied the check for the site block's. A stray closing brace after a braceless site was likewise accepted as the end of the block. Track whether the directives of a block were terminated by the block's own closing brace: running out of tokens in a braced block is an error, and a closing brace in a braceless block is an error. Fixes #8374
Hafiar committed
Nov 8, 2026 at 18:16 UTC
2563d4832640f7748c60657d53e7e4274fc73d31
3 files changed
+94
-15
caddyconfig/caddyfile/formatter_braced_test.go
+10
-7
@@ -47,14 +47,17 @@ func TestWrapUnbracedSiteIdempotentAndSemantic(t *testing.T) {
47
}
48
49
func TestWrapUnbracedSiteIdempotentReproducer(t *testing.T) {
50
- // A valid Caddyfile (Parse accepts it) that used to be non-idempotent under
51
- // WrapUnbracedSite: it reads as "not a single site" on the raw stream (a
52
- // blank-line gap), but after formatTokens removes the blank line and folds a
53
- // dangling brace, a second pass saw a single unbraced site and wrapped it.
54
- // Deciding on the normalized stream makes the wrap decision stable.
50
+ // An input that used to be non-idempotent under WrapUnbracedSite: it reads
51
+ // as "not a single site" on the raw stream (a blank-line gap), but after
52
+ // formatTokens removes the blank line and folds a dangling brace, a second
53
+ // pass saw a single unbraced site and wrapped it. Deciding on the
54
+ // normalized stream makes the wrap decision stable. Parse rejects this
55
+ // input since the fix for unbalanced braces in site blocks (the stray "}"
56
+ // after a braceless site is an error), but the formatter works on the
57
+ // token stream and must stay idempotent on it regardless.
58
in := []byte(") 0((.b ))\na\t\tab(\n}\n\n { }")
56
- if _, err := Parse("", in); err != nil {
57
- t.Fatalf("reproducer must be a valid Caddyfile, got parse error: %v", err)
59
+ if _, err := Parse("", in); err == nil {
60
+ t.Fatalf("reproducer should be rejected by Parse (unbalanced braces), got no error")
61
}
62
once := FormatWithOptions(in, FormatOptions{WrapUnbracedSite: true})
63
twice := FormatWithOptions(once, FormatOptions{WrapUnbracedSite: true})
caddyconfig/caddyfile/parse.go
+21
-8
@@ -315,14 +315,21 @@ func (p *parser) blockContents() error {
315
// single-server configs don't need curly braces
316
p.cursor--
317
}
318
+ braced := errOpenCurlyBrace == nil
319
319
- err := p.directives()
320
+ closed, err := p.directives(braced)
321
if err != nil {
322
return err
323
}
324
325
// only look for close curly brace if there was an opening
325
- if errOpenCurlyBrace == nil {
326
+ if braced {
327
+ if !closed {
328
+ // the tokens ran out before the server block was closed;
329
+ // the current token may be the closing brace of a nested
330
+ // directive block, which does not close this block
331
+ return p.EOFErr()
332
+ }
333
err = p.closeCurlyBrace()
334
if err != nil {
335
return err
@@ -335,20 +342,26 @@ func (p *parser) blockContents() error {
342
// directives parses through all the lines for directives
343
// and it expects the next token to be the first
344
// directive. It goes until EOF or closing curly brace
338
-// which ends the server block.
339
-func (p *parser) directives() error {
345
+// which ends the server block. It reports whether it
346
+// stopped at the closing curly brace of a braced server
347
+// block (braced); a closing curly brace is an error if
348
+// the server block was not opened with one.
349
+func (p *parser) directives(braced bool) (bool, error) {
350
for p.Next() {
351
// end of server block
352
if isCloseCurlyBrace(p.Token()) {
353
+ if !braced {
354
+ return false, p.Err("Unexpected '}' because no matching opening brace")
355
+ }
356
// p.nesting has already been decremented
344
- break
357
+ return true, nil
358
}
359
360
// special case: import directive replaces tokens during parse-time
361
if p.Val() == "import" {
362
err := p.doImport(1)
363
if err != nil {
351
- return err
364
+ return false, err
365
}
366
p.cursor-- // cursor is advanced when we continue, so roll back one more
367
continue
@@ -359,11 +372,11 @@ func (p *parser) directives() error {
372
// and which ends at the end of the line or at the end of
373
// the block that is opened at the end of the line)
374
if err := p.directive(); err != nil {
362
- return err
375
+ return false, err
376
}
377
}
378
366
- return nil
379
+ return false, nil
380
}
381
382
// doImport swaps out the import directive and its argument
caddyconfig/caddyfile/parse_test.go
+63
@@ -651,6 +651,69 @@ func TestParseAll(t *testing.T) {
651
:80
652
import A
653
`, true, [][]string{}},
654
+
655
+ // missing closing brace of the site block; the last directive
656
+ // having its own block must not mask it (issue #8374)
657
+ {`:8080 {
658
+ header {
659
+ X-Foo "bar"
660
+ }
661
+ `, true, [][]string{}},
662
+
663
+ // same, with the last directive's block written inline-nested
664
+ {`:8080 {
665
+ file_server {
666
+ browse
667
+ }
668
+ `, true, [][]string{}},
669
+
670
+ // unclosed global options block whose last directive has a block
671
+ {`{
672
+ servers {
673
+ protocols h1
674
+ }
675
+ `, true, [][]string{}},
676
+
677
+ // missing closing brace, last directive has no block
678
+ {`:8080 {
679
+ file_server
680
+ `, true, [][]string{}},
681
+
682
+ // extra closing brace after a site without braces (issue #8374)
683
+ {`:8080
684
+ file_server
685
+ }
686
+ `, true, [][]string{}},
687
+
688
+ // a closing brace must not separate two braceless sites
689
+ {`:8080
690
+ root /www/example.com
691
+ file_server
692
+ }
693
+
694
+ :3000
695
+ reverse_proxy localhost:9000
696
+ `, true, [][]string{}},
697
+
698
+ // valid controls: nested directive block as the last directive,
699
+ // site block closed afterwards
700
+ {`:8080 {
701
+ header {
702
+ X-Foo "bar"
703
+ }
704
+ }`, false, [][]string{
705
+ {":8080"},
706
+ }},
707
+
708
+ {`:8080 {
709
+ handle {
710
+ header {
711
+ X-Foo "bar"
712
+ }
713
+ }
714
+ }`, false, [][]string{
715
+ {":8080"},
716
+ }},
717
} {
718
p := testParser(test.input)
719
blocks, err := p.parseAll()