@marcel91200 / caddy Mirror · read-only / commits / c1acb2fc
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, 03:30 UTC

caddyfile: surface OS error when an import file is inaccessible (#8163)

* caddyfile: surface OS error when an import file is inaccessible filepath.Glob ignores file system errors, so an import path that exists but cannot be stat'ed (e.g. permission denied on a parent directory) was reported as 'File to import not found'. Stat the literal path when Glob finds no matches and report the real error unless the file genuinely does not exist. Fixes #8161 * caddyfile: skip through-a-file import case on Windows Windows reports a path through a regular file as not existing rather than ENOTDIR, so os.Stat cannot distinguish it from a missing file there. The permission-denied case from #8161 is unaffected.

KShot committed Oct 6, 2026 at 22:05 UTC c1acb2fcbac9c659f601ad3a920653ac959ff952
2 files changed +46
caddyconfig/caddyfile/parse.go
+8
@@ -450,6 +450,14 @@ func (p *parser) doImport(nesting int) error {
450 if strings.ContainsAny(globPattern, "*?[]") {
451 caddy.Log().Warn("No files matching import glob pattern", zap.String("pattern", importPattern))
452 } else {
453 + // filepath.Glob ignores file system errors (it only
454 + // returns ErrBadPattern), so a file that exists but
455 + // cannot be accessed - e.g. permission denied - looks
456 + // the same as a missing file. Stat the path directly
457 + // so the real error is reported instead (issue #8161).
458 + if _, err := os.Stat(globPattern); err != nil && !os.IsNotExist(err) {
459 + return p.Errf("Failed to import file %s: %v", importPattern, err)
460 + }
461 return p.Errf("File to import not found: %s", importPattern)
462 }
463 } else {
caddyconfig/caddyfile/parse_test.go
+38
@@ -18,6 +18,7 @@ import (
18 "bytes"
19 "os"
20 "path/filepath"
21 + "runtime"
22 "strings"
23 "testing"
24 )
@@ -479,6 +480,43 @@ func TestRecursiveImport(t *testing.T) {
480 }
481 }
482
483 +// TestParseImportInaccessibleFile ensures that an import path which exists
484 +// but cannot be accessed reports the underlying OS error instead of the
485 +// misleading "File to import not found" (issue #8161). A path through a
486 +// regular file fails stat with ENOTDIR, unlike a genuinely missing file,
487 +// which must keep the "not found" message. Windows reports that same path
488 +// as not existing instead, so the through-a-file case is skipped there;
489 +// the permission-denied case from the issue is unaffected.
490 +func TestParseImportInaccessibleFile(t *testing.T) {
491 + testParseOne := func(input string) (ServerBlock, error) {
492 + p := testParser(input)
493 + p.Next() // parseOne doesn't call Next() to start, so we must
494 + err := p.parseOne()
495 + return p.block, err
496 + }
497 +
498 + if runtime.GOOS != "windows" {
499 + _, err := testParseOne("import testdata/import_test1.txt/child.txt")
500 + if err == nil {
501 + t.Fatal("expected error importing through a regular file, got nil")
502 + }
503 + if strings.Contains(err.Error(), "not found") {
504 + t.Errorf("expected OS error to be reported, got: %v", err)
505 + }
506 + if !strings.Contains(err.Error(), "child.txt") {
507 + t.Errorf("expected error to name the import path, got: %v", err)
508 + }
509 + }
510 +
511 + _, err := testParseOne("import testdata/not_found.txt")
512 + if err == nil {
513 + t.Fatal("expected error importing a missing file, got nil")
514 + }
515 + if !strings.Contains(err.Error(), "File to import not found") {
516 + t.Errorf("expected 'File to import not found' for a missing file, got: %v", err)
517 + }
518 +}
519 +
520 func TestDirectiveImport(t *testing.T) {
521 testParseOne := func(input string) (ServerBlock, error) {
522 p := testParser(input)