8000 [pull] main from miniflux:main by pull[bot] · Pull Request #227 · suhaoGe/miniflux · GitHub
[go: up one dir, main page]

Skip to content
Merged 8000
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions internal/reader/sanitizer/sanitizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package sanitizer // import "miniflux.app/v2/internal/reader/sanitizer"

import (
"errors"
"net/url"
"slices"
"strconv"
Expand All @@ -16,6 +17,10 @@ import (
"golang.org/x/net/html"
)

const (
maxDepth = 512 // The maximum allowed depths for nested HTML tags, same was WebKit.
)

var (
allowedHTMLTagsAndAttributes = map[string][]string{
"a": {"href", "title", "id"},
Expand Down Expand Up @@ -129,14 +134,16 @@ var (

blockedResourceURLSubstrings = []string{
"api.flattr.com",
"www.facebook.com/sharer.php",
"feeds.feedburner.com",
"feedsportal.com",
"linkedin.com/shareArticle",
"pinterest.com/pin/create/button/",
"stats.wordpress.com",
"twitter.com/intent/tweet",
"twitter.com/share",
"facebook.com/sharer.php",
"linkedin.com/shareArticle",
"x.com/intent/tweet",
"x.com/share",
}

// See https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
Expand Down Expand Up @@ -231,7 +238,10 @@ func SanitizeHTML(baseURL, rawHTML string, sanitizerOptions *SanitizerOptions) s
// Errors are a non-issue, so they're handled in filterAndRenderHTML
parsedBaseUrl, _ := url.Parse(baseURL)
for c := body.FirstChild; c != nil; c = c.NextSibling {
filterAndRenderHTML(&buffer, c, parsedBaseUrl, sanitizerOptions)
// -2 because of `<html><body>…`
if err := filterAndRenderHTML(&buffer, c, parsedBaseUrl, sanitizerOptions, maxDepth-2); err != nil {
return ""
}
}

return buffer.String()
Expand All @@ -255,9 +265,13 @@ func findAllowedIframeSourceDomain(iframeSourceURL string) (string, bool) {
return "", false
}

func filterAndRenderHTML(buf *strings.Builder, n *html.Node, parsedBaseUrl *url.URL, sanitizerOptions *SanitizerOptions) {
func filterAndRenderHTML(buf *strings.Builder, n *html.Node, parsedBaseUrl *url.URL, sanitizerOptions *SanitizerOptions, depth uint) error {
if n == nil {
return
return nil
}

if depth == 0 {
return errors.New("maximum nested tags limit reached")
}

switch n.Type {
Expand All @@ -266,21 +280,19 @@ func filterAndRenderHTML(buf *strings.Builder, n *html.Node, parsedBaseUrl *url.
case html.ElementNode:
tag := strings.ToLower(n.Data)
if shouldIgnoreTag(n, tag) {
return
return nil
}

_, ok := allowedHTMLTagsAndAttributes[tag]
if !ok {
// The tag isn't allowed, but we're still interested in its content
filterAndRenderHTMLChildren(buf, n, parsedBaseUrl, sanitizerOptions)
return
return filterAndRenderHTMLChildren(buf, n, parsedBaseUrl, sanitizerOptions, depth-1)
}

htmlAttributes, hasAllRequiredAttributes := sanitizeAttributes(parsedBaseUrl, tag, n.Attr, sanitizerOptions)
if !hasAllRequiredAttributes {
// The tag doesn't have every required attributes but we're still interested in its content
filterAndRenderHTMLChildren(buf, n, parsedBaseUrl, sanitizerOptions)
return
return filterAndRenderHTMLChildren(buf, n, parsedBaseUrl, sanitizerOptions, depth-1)
}
buf.WriteString("<")
buf.WriteString(n.Data)
Expand All @@ -290,25 +302,29 @@ func filterAndRenderHTML(buf *strings.Builder, n *html.Node, parsedBaseUrl *url.
buf.WriteString(">")

if isSelfContainedTag(tag) {
return
return nil
}

if tag != "iframe" {
// iframes aren't allowed to have child nodes.
filterAndRenderHTMLChildren(buf, n, parsedBaseUrl, sanitizerOptions)
filterAndRenderHTMLChildren(buf, n, parsedBaseUrl, sanitizerOptions, depth-1)
}

buf.WriteString("</")
buf.WriteString(n.Data)
buf.WriteString(">")
default:
}
return nil
}

func filterAndRenderHTMLChildren(buf *strings.Builder, n *html.Node, parsedBaseUrl *url.URL, sanitizerOptions *SanitizerOptions) {
func filterAndRenderHTMLChildren(buf *strings.Builder, n *html.Node, parsedBaseUrl *url.URL, sanitizerOptions *SanitizerOptions, depth uint) error {
for c := n.FirstChild; c != nil; c = c.NextSibling {
filterAndRenderHTML(buf, c, parsedBaseUrl, sanitizerOptions)
if err := filterAndRenderHTML(buf, c, parsedBaseUrl, sanitizerOptions, depth); err != nil {
return err
}
}
return nil
}

func getExtraAttributes(tagName string, isYouTubeEmbed bool, sanitizerOptions *SanitizerOptions) []string {
Expand Down Expand Up @@ -564,13 +580,17 @@ func sanitizeAttributes(parsedBaseUrl *url.URL, tagName string, attributes []htm
value = attribute.Val
isAnchorLink = true
default:
if isBlockedResource(value) {
continue
}

var err error
value, err = urllib.ResolveToAbsoluteURLWithParsedBaseURL(parsedBaseUrl, value)
if err != nil {
continue
}

if !hasValidURIScheme(value) || isBlockedResource(value) {
if !hasValidURIScheme(value) {
continue
}

Expand Down
24 changes: 24 additions & 0 deletions internal/reader/sanitizer/sanitizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -966,3 +966,27 @@ func TestAttrLowerCase(t *testing.T) {
})
}
}

func TestDeeplyNestedpage(t *testing.T) {
input := "test"
// -3 instead of -1 because <html><body> is automatically added.
for range maxDepth - 3 {
input = "<div>" + input + "</div>"
}
output := sanitizeHTMLWithDefaultOptions("http://example.org/", input)
want := "test"

if output != want {
t.Errorf(`Wrong output: "%s" != "%s"`, want, output)
}

input = "test"
for range maxDepth - 2 {
input = "<div>" + input + "</div>"
}
output = sanitizeHTMLWithDefaultOptions("http://example.org/", input)

if output != "" {
t.Errorf(`Wrong output: "%s" != "%s"`, "", output)
}
}
0