Skip to content

Commit

Permalink
tpl/tplimpl: Prevent overloading of embedded comment shortcode
Browse files Browse the repository at this point in the history
Creates a mechanism to prevent loading of user space templates that
match a list of reserved paths. The primary intent is to prevent users
from overloading specific embedded templates. For example, with the
string "shortcodes/comment" in the list of reserved paths, we do not
load any of the following from user space:

  - layouts/shortcodes/comment.html
  - layouts/shortcodes/comment.html.html
  - layouts/shortcodes/comment.en.html.html
  - layouts/shortcodes/comment.json"
  - layouts/shortcodes/comment.json.json"
  - layouts/shortcodes/comment.en.json.json"
  • Loading branch information
jmooring committed Jan 18, 2025
1 parent 8658b77 commit 88dd3ff
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 0 deletions.
12 changes: 12 additions & 0 deletions common/paths/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,15 @@ func ToSlashPreserveLeading(s string) string {
func IsSameFilePath(s1, s2 string) bool {
return path.Clean(ToSlashTrim(s1)) == path.Clean(ToSlashTrim(s2))
}

// ToSlashNoExtensions returns the result of removing all file extensions from
// path s, then replacing each separator character with a slash ('/')
// character. For example, "/a/b/c.d/e.f.g" becomes "/a/b/c.d/e".
func ToSlashNoExtensions(s string) string {
d, f := filepath.Split(s)
i := strings.Index(f, ".")
if i >= 0 {
f = f[:i]
}
return filepath.ToSlash(path.Join(d, f))
}
39 changes: 39 additions & 0 deletions common/paths/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package paths

import (
"path/filepath"
"strconv"
"testing"

qt "github.com/frankban/quicktest"
Expand Down Expand Up @@ -311,3 +312,41 @@ func TestIsSameFilePath(t *testing.T) {
c.Assert(IsSameFilePath(filepath.FromSlash(this.a), filepath.FromSlash(this.b)), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
}
}

func TestToSlashNoExtensions(t *testing.T) {
tests := []struct {
path string
want string
}{
{"a", "a"},
{"a/", "a"},
{"/a", "/a"},
{"/a/", "/a"},
{"a.b", "a"},
{"a.b/", "a.b"},
{"/a.b", "/a"},
{"/a.b/", "/a.b"},
{"a/b", "a/b"},
{"a/b/", "a/b"},
{"a/b", "a/b"},
{"/a/b/", "/a/b"},
{"a/b/c.d", "a/b/c"},
{"a/b/c.d", "a/b/c"},
{"a/b/c.d.e", "a/b/c"},
{"a/b/c.d/e", "a/b/c.d/e"},
{"a/b/c.d/e.f", "a/b/c.d/e"},
{"a/b/c.d/e.f.g", "a/b/c.d/e"},
{"a.", "a"},
{".a", ""},
{"/", "/"},
{".", ""},
{"", ""},
}
for k, tt := range tests {
t.Run(strconv.Itoa(k), func(t *testing.T) {
if got := ToSlashNoExtensions(tt.path); got != tt.want {
t.Errorf("ToSlashNoExtensions() = %v, want %v", got, tt.want)
}
})
}
}
23 changes: 23 additions & 0 deletions tpl/tplimpl/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"path/filepath"
"reflect"
"regexp"
"slices"
"sort"
"strings"
"sync"
Expand All @@ -31,6 +32,7 @@ import (
"unicode/utf8"

"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/output/layouts"

Expand Down Expand Up @@ -72,6 +74,14 @@ var embeddedTemplatesAliases = map[string][]string{
"shortcodes/twitter.html": {"shortcodes/tweet.html"},
}

// These paths are reserved for embedded templates.
// They follow the format "directory/name" relative to the layouts directory,
// omitting any file extensions. Templates with these paths are not loaded from
// user space.
var reservedTemplatePaths = []string{
"shortcodes/comment",
}

var (
_ tpl.TemplateManager = (*templateExec)(nil)
_ tpl.TemplateHandler = (*templateExec)(nil)
Expand Down Expand Up @@ -825,6 +835,12 @@ func (t *templateHandler) loadTemplates() error {

name := strings.TrimPrefix(filepath.ToSlash(path), "/")
filename := filepath.Base(path)

if t.isReservedTemplatePath(path) {
t.Log.Infof("template not loaded: the path %q is reserved for embedded templates", name)
return nil
}

outputFormats := t.Conf.GetConfigSection("outputFormats").(output.Formats)
outputFormat, found := outputFormats.FromFilename(filename)

Expand All @@ -849,6 +865,13 @@ func (t *templateHandler) loadTemplates() error {
return nil
}

// isReservedTemplatePath reports whether the template with the given name is
// reserved for embedded templates.
func (t *templateHandler) isReservedTemplatePath(path string) bool {
// shortcodesPathPrefix
return slices.Contains(reservedTemplatePaths, paths.ToSlashNoExtensions(path))
}

func (t *templateHandler) nameIsText(name string) (string, bool) {
isText := strings.HasPrefix(name, textTmplNamePrefix)
if isText {
Expand Down
34 changes: 34 additions & 0 deletions tpl/tplimpl/tplimpl_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -858,3 +858,37 @@ title: p5
}
b.Assert(htmlFiles, hqt.IsAllElementsEqual)
}

func TestFoo(t *testing.T) {
t.Parallel()

files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
-- layouts/LAYOUT --
`

filesOriginal := files

layouts := []string{
"shortcodes/comment.html",
"shortcodes/comment.html.html",
"shortcodes/comment.en.html.html",
"shortcodes/comment.de.html.html",
"shortcodes/comment.json",
"shortcodes/comment.json.json",
"shortcodes/comment.en.json.json",
"shortcodes/comment.de.json.json",
}

// want := `INFO template not loaded: the path "shortcodes/comment.html" is reserved for embedded templates`
for _, layout := range layouts {
files = strings.ReplaceAll(filesOriginal, "LAYOUT", layout)
b := hugolib.Test(t, files, hugolib.TestOptInfo())
b.AssertLogContains("INFO template not loaded: the path", layout, "is reserved for embedded templates")
}

files = strings.ReplaceAll(filesOriginal, "LAYOUT", "shortcodes/foo.html")
b := hugolib.Test(t, files, hugolib.TestOptInfo())
b.AssertLogContains("! INFO template not loaded: the path", "! is reserved for embedded templates")
}

0 comments on commit 88dd3ff

Please sign in to comment.