]> source.dussan.org Git - gitea.git/commitdiff
Use restricted sanitizer for repository description (#28141)
authorEarl Warren <109468362+earl-warren@users.noreply.github.com>
Thu, 23 Nov 2023 16:34:25 +0000 (17:34 +0100)
committerGitHub <noreply@github.com>
Thu, 23 Nov 2023 16:34:25 +0000 (16:34 +0000)
- Currently the repository description uses the same sanitizer as a
normal markdown document. This means that element such as heading and
images are allowed and can be abused.
- Create a minimal restricted sanitizer for the repository description,
which only allows what the postprocessor currently allows, which are
links and emojis.
- Added unit testing.
- Resolves https://codeberg.org/forgejo/forgejo/issues/1202
- Resolves https://codeberg.org/Codeberg/Community/issues/1122

(cherry picked from commit 631c87cc2347f0036a75dcd21e24429bbca28207)

Co-authored-by: Gusted <postmaster@gusted.xyz>
models/repo/repo.go
modules/markup/sanitizer.go
modules/markup/sanitizer_test.go

index c4b215e074ecdf99d4c0a960ab3d9d99a765d0a9..db3709f1e802512f7a7f9bbd17ff29e67a65521c 100644 (file)
@@ -584,9 +584,9 @@ func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
        }, repo.Description)
        if err != nil {
                log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
-               return template.HTML(markup.Sanitize(repo.Description))
+               return template.HTML(markup.SanitizeDescription(repo.Description))
        }
-       return template.HTML(markup.Sanitize(desc))
+       return template.HTML(markup.SanitizeDescription(desc))
 }
 
 // CloneLink represents different types of clone URLs of repository.
index 48c08831f1661311565f40c219d7199b3bcd470a..992e85b9898d9330c4bf31fc02a7057617f57447 100644 (file)
@@ -18,9 +18,10 @@ import (
 // Sanitizer is a protection wrapper of *bluemonday.Policy which does not allow
 // any modification to the underlying policies once it's been created.
 type Sanitizer struct {
-       defaultPolicy    *bluemonday.Policy
-       rendererPolicies map[string]*bluemonday.Policy
-       init             sync.Once
+       defaultPolicy     *bluemonday.Policy
+       descriptionPolicy *bluemonday.Policy
+       rendererPolicies  map[string]*bluemonday.Policy
+       init              sync.Once
 }
 
 var (
@@ -41,6 +42,7 @@ func NewSanitizer() {
 func InitializeSanitizer() {
        sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
        sanitizer.defaultPolicy = createDefaultPolicy()
+       sanitizer.descriptionPolicy = createRepoDescriptionPolicy()
 
        for name, renderer := range renderers {
                sanitizerRules := renderer.SanitizerRules()
@@ -161,6 +163,27 @@ func createDefaultPolicy() *bluemonday.Policy {
        return policy
 }
 
+// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
+// repository descriptions.
+func createRepoDescriptionPolicy() *bluemonday.Policy {
+       policy := bluemonday.NewPolicy()
+
+       // Allow italics and bold.
+       policy.AllowElements("i", "b", "em", "strong")
+
+       // Allow code.
+       policy.AllowElements("code")
+
+       // Allow links
+       policy.AllowAttrs("href", "target", "rel").OnElements("a")
+
+       // Allow classes for emojis
+       policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
+       policy.AllowAttrs("aria-label").OnElements("span")
+
+       return policy
+}
+
 func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
        for _, rule := range rules {
                if rule.AllowDataURIImages {
@@ -176,6 +199,12 @@ func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitize
        }
 }
 
+// SanitizeDescription sanitizes the HTML generated for a repository description.
+func SanitizeDescription(s string) string {
+       NewSanitizer()
+       return sanitizer.descriptionPolicy.Sanitize(s)
+}
+
 // Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
 func Sanitize(s string) string {
        NewSanitizer()
index 0bc63ff0a7cb3002049f64329cec278ee430c0c4..b7b8792bd73bd4c8fe09c23d6d3ab88408f5d141 100644 (file)
@@ -73,6 +73,28 @@ func Test_Sanitizer(t *testing.T) {
        }
 }
 
+func TestDescriptionSanitizer(t *testing.T) {
+       NewSanitizer()
+
+       testCases := []string{
+               `<h1>Title</h1>`, `Title`,
+               `<img src='img.png' alt='image'>`, ``,
+               `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
+               `<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
+               `<br>`, ``,
+               `<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`,
+               `<mark>Important!</mark>`, `Important!`,
+               `<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
+               `<input type="hidden">`, ``,
+               `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
+               `Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
+       }
+
+       for i := 0; i < len(testCases); i += 2 {
+               assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
+       }
+}
+
 func TestSanitizeNonEscape(t *testing.T) {
        descStr := "<scrİpt>&lt;script&gt;alert(document.domain)&lt;/script&gt;</scrİpt>"