@marcel91200 / caddy Mirror · read-only / commits / 2563d483
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 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()