Fix prevention of direct symlink reads in resources.Get

* Note for themes, this is only an issue for themes stored locally, e.g. below `themes/...`. Themes mounted as modules from GitHub gets symlinks stripped away.
* Thas was also not an issue for file reading walking one or more directories.
* This is an regression introduced in `v0.123.0`.
This commit is contained in:
Bjørn Erik Pedersen
2026-05-11 12:52:53 +02:00
parent 86fbb0f7a8
commit f8b5fa09a6
5 changed files with 90 additions and 1 deletions
+16
View File
@@ -106,6 +106,22 @@ func (fs *baseFileDecoratorFs) Stat(name string) (os.FileInfo, error) {
return fim.(os.FileInfo), nil
}
func (fs *baseFileDecoratorFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
if lstater, ok := fs.Fs.(afero.Lstater); ok {
fi, ok, err := lstater.LstatIfPossible(name)
if err != nil {
return nil, false, err
}
fim, err := fs.decorate(fi, name)
if err != nil {
return nil, false, err
}
return fim.(os.FileInfo), ok, nil
}
fi, err := fs.Stat(name)
return fi, false, err
}
func (fs *baseFileDecoratorFs) Open(name string) (afero.File, error) {
return fs.open(name)
}
+10
View File
@@ -267,3 +267,13 @@ func ReadDirWithContext(ctx context.Context, f DirOnlyOps, count int) ([]iofs.Di
}
return v, ctx, nil
}
// LstatIfPossible tries to use LstatIfPossible if the filesystem supports it, otherwise it falls back to Stat.
func LstatIfPossible(fs afero.Fs, name string) (os.FileInfo, error) {
if lstater, ok := fs.(afero.Lstater); ok {
fi, _, err := lstater.LstatIfPossible(name)
return fi, err
}
fi, err := fs.Stat(name)
return fi, err
}
+6 -1
View File
@@ -724,11 +724,16 @@ func (fs *RootMappingFs) statRoot(root *RootMapping, filename string) (FileMetaI
}
filename = root.filename(filename)
fi, err := fs.Fs.Stat(filename)
fi, err := LstatIfPossible(fs.Fs, filename)
if err != nil {
return nil, err
}
// Don't allow symlinks to escape the mount.
if fi.Mode()&os.ModeSymlink != 0 {
return nil, os.ErrNotExist
}
var opener func() (afero.File, error)
if !fi.IsDir() {
// Open the file directly.
+19
View File
@@ -237,6 +237,21 @@ var commonTestScriptsParam = testscript.Params{
ts.Fatalf("failed to write file: %v", err)
}
},
// ln creates a symlink, but throws an error on Windows.
"ln": func(ts *testscript.TestScript, neg bool, args []string) {
if runtime.GOOS == "windows" {
ts.Fatalf("ln is not supported on Windows")
}
if len(args) != 2 {
ts.Fatalf("usage: ln TARGET LINKNAME")
}
target := ts.MkAbs(args[0])
linkname := ts.MkAbs(args[1])
err := os.Symlink(target, linkname)
if err != nil {
ts.Fatalf("failed to create symlink: %v", err)
}
},
// httpget checks that a HTTP resource's body matches (if it compiles as a regexp) or contains all of the strings given as arguments.
"httpget": func(ts *testscript.TestScript, neg bool, args []string) {
@@ -314,6 +329,10 @@ var commonTestScriptsParam = testscript.Params{
if !ok {
ts.Fatalf("stat %s: %v", filename, err)
}
if ok && neg {
// OK.
continue
}
if fi.Size() == 0 {
ts.Fatalf("%s is empty", filename)
}
+39
View File
@@ -0,0 +1,39 @@
[windows] skip
ln ./rootfile.txt ./themes/mytheme/assets/modassetsymlink.txt
ln ./rootfile.txt ./themes/mytheme/static/modstaticsymlink.txt
ln ./README.md ./content/pagesymlink.md
hugo
grep 'OK' public/index.html
! grep 'FAIL' public/index.html
tree public
stdout modassetok
! stdout modassetsymlink
stdout 'modstatictok'
! stdout 'modstaticsymlink'
stdout pageok
! stdout pagesymlink
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss"]
[[module.imports]]
path = 'mytheme'
-- README.md --
Read me.
-- layouts/all.html --
{{ with resources.Get "modassetok.txt"}}OK {{ .Publish }}{{ else }}FAIL{{ end }}
{{ with resources.Get "modassetsymlink.txt"}}FAIL {{ .Publish }}{{ else }}OK{{ end }}
Page: {{ .RelPermalink }}|{{ .Content }}|
-- content/pageok.md --
-- themes/mytheme/assets/modassetok.txt --
Content.
-- themes/mytheme/static/modstatictok.txt --
Content.
-- rootfile.txt --
Roo Content.