diff --git a/go.mod b/go.mod index 4de91dd5d..a745e4c57 100644 --- a/go.mod +++ b/go.mod @@ -123,6 +123,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect github.com/aws/smithy-go v1.22.2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect @@ -141,6 +142,7 @@ require ( github.com/google/wire v0.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -151,6 +153,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect diff --git a/go.sum b/go.sum index 0168642e8..76c5702e3 100644 --- a/go.sum +++ b/go.sum @@ -137,6 +137,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= @@ -352,6 +354,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo= @@ -406,6 +410,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= diff --git a/markup/goldmark/toc.go b/markup/goldmark/toc.go index 538f65df4..b4dcc425f 100644 --- a/markup/goldmark/toc.go +++ b/markup/goldmark/toc.go @@ -15,7 +15,10 @@ package goldmark import ( "bytes" + "regexp" + "strings" + "github.com/microcosm-cc/bluemonday" strikethroughAst "github.com/yuin/goldmark/extension/ast" emojiAst "github.com/yuin/goldmark-emoji/ast" @@ -61,7 +64,7 @@ func (t *tocTransformer) Transform(n *ast.Document, reader text.Reader, pc parse s := ast.WalkStatus(ast.WalkContinue) if n.Kind() == ast.KindHeading { if inHeading && !entering { - tocHeading.Title = headingText.String() + tocHeading.Title = sanitizeTOCHeadingTitle(headingText.String()) headingText.Reset() toc.AddAt(tocHeading, row, level-1) tocHeading = &tableofcontents.Heading{} @@ -139,3 +142,40 @@ func (e *tocExtension) Extend(m goldmark.Markdown) { // This must run after the ID generation (priority 100). 110))) } + +var tocSanitizerPolicy = newTOCSanitizerPolicy() + +// newTOCSanitizerPolicy returns a bluemonday policy for sanitizing TOC heading +// titles against an allowlist of inline HTML elements and attributes, +// specifically excluding anchor elements to prevent links within TOC heading +// titles. +func newTOCSanitizerPolicy() *bluemonday.Policy { + p := bluemonday.NewPolicy() + p.AllowElements( + "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "del", "dfn", + "em", "i", "ins", "kbd", "mark", "q", "rp", "rt", "ruby", "s", "samp", + "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", + ) + p.AllowStandardAttributes() + p.AllowStyling() + p.AllowImages() + p.AllowAttrs("cite").OnElements("del", "ins", "q") + p.AllowAttrs("datetime").OnElements("del", "ins", "time") + p.AllowAttrs("value").OnElements("data") + return p +} + +var whiteSpaceRe = regexp.MustCompile(`\s+`) + +// sanitizeTOCHeadingTitle sanitizes s for use as a TOC heading title. +func sanitizeTOCHeadingTitle(s string) string { + if strings.IndexByte(s, '<') == -1 { + return s + } + + // Sanitize the string. + ss := tocSanitizerPolicy.Sanitize(s) + + // Remove extraneous whitespace. + return whiteSpaceRe.ReplaceAllString(strings.TrimSpace(ss), " ") +} diff --git a/markup/goldmark/toc_integration_test.go b/markup/goldmark/toc_integration_test.go index 814ae199b..b4dfeee05 100644 --- a/markup/goldmark/toc_integration_test.go +++ b/markup/goldmark/toc_integration_test.go @@ -90,6 +90,11 @@ title: p6 (strikethrough) title: p7 (emoji) --- ## A :snake: emoji +-- content/p8.md -- +--- +title: p8 (link) +--- +## A [link](https://example.org) ` b := hugolib.Test(t, files) @@ -111,36 +116,41 @@ title: p7 (emoji) `) // markdown - b.AssertFileContent("public/p2/index.html", ``) // markdown - b.AssertFileContent("public/p2/index.html", `