diff --git a/modules/image/svg.go b/modules/image/svg.go new file mode 100644 index 0000000000000..b26fe04b45fd4 --- /dev/null +++ b/modules/image/svg.go @@ -0,0 +1,46 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package image + +import ( + "io" + "regexp" + "strings" + + "github.com/microcosm-cc/bluemonday" +) + +// SanitizeSVG remove potential malicious dom elements +func SanitizeSVG(svgData io.Reader) string { + //TODO init policy at start-up and keep it + p := bluemonday.NewPolicy() + p.AllowElements("svg", "title", "path", "desc", "g", "a", "line") + p.AllowNoAttrs().OnElements("svg", "title", "desc", "g", "a") + p.AllowAttrs("id", "viewBox", "role", "aria-labelledby", "xmlns", "xmlns:xlink", "xml:space").OnElements("svg") + p.AllowAttrs("version").Matching(regexp.MustCompile(`^\d+(\.\d+)?$`)).OnElements("svg") + p.AllowAttrs("id").OnElements("title", "desc") + p.AllowAttrs("id", "data-name", "class", "aria-label").OnElements("g") + p.AllowAttrs("id", "data-name", "class", "d", "transform", "aria-haspopup").OnElements("path") + p.AllowAttrs("x", "y", "width", "height").OnElements("rect", "svg") + p.AllowAttrs("x1", "y1", "x2", "y2").Matching(regexp.MustCompile(`^\d+(\.\d+)?$`)).OnElements("line") + p.AllowAttrs("stroke-miterlimit").Matching(regexp.MustCompile(`^\d+$`)).OnElements("line") + p.AllowAttrs("stroke", "fill").Matching(regexp.MustCompile(`^(#\d+)|(\w+)$`)).OnElements("line", "rect") + p.AllowAttrs("href", "xlink:href").Matching(regexp.MustCompile(`^#\w+$`)).OnElements("a") + + //TODO find a good way to allow relative url import + //var invalidID = regexp.MustCompile(`((http|ftp)s?)|(url *\( *' *//)`) + //var validID = regexp.MustCompile(`(?!((http|ftp)s?)|(url *\( *' *//))`) //not supported + //p.AllowAttrs("fill").Matching(regexp.MustCompile(`^(\w+)|(url\(#\w+\))$`)).OnElements("rect") + + p.SkipElementsContent("this", "script") + cleanedSVG := p.SanitizeReader(svgData).String() + + //Remove empty lines + cleanedSVG = strings.TrimSpace(cleanedSVG) + r := regexp.MustCompile("\n+") //TODO move this somewhere else + cleanedSVG = r.ReplaceAllString(cleanedSVG, "\n") + + return cleanedSVG +} diff --git a/modules/image/svg_test.go b/modules/image/svg_test.go new file mode 100644 index 0000000000000..29c6b2bcf8058 --- /dev/null +++ b/modules/image/svg_test.go @@ -0,0 +1,345 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package image + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSanitizeSVG(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "ariaData", + input: ` + Pixels, My Super-friendly Cat + An illustrated gray cat with bright green blinking eyes. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`, + /* Adjustement from https://github.com/darylldoyle/svg-sanitizer base + Diff: + --- Expected + +++ Actual + @@ -1,2 +1,2 @@ + - + + + */ + want: ` + Pixels, My Super-friendly Cat + An illustrated gray cat with bright green blinking eyes. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`, + }, + { + name: "badXmlTestOne", + input: ` + + + + + + + + + +shouldn't be here + + +`, + /* Adjustement from https://github.com/darylldoyle/svg-sanitizer base + --- Expected + +++ Actual + @@ -1,2 +1,9 @@ + - + + + + + + + + + + + + + + + + + */ + want: ` + + + + + + + +`, + }, + { + name: "externalTest", + input: ` + + + + + + + + + +`, + /* Adjustement from https://github.com/darylldoyle/svg-sanitizer base + --- Expected + +++ Actual + @@ -1,2 +1,2 @@ + -110 + - + + + @@ -6,4 +6,4 @@ + + - + - + + + + + FIXME: Currently this will block any fill with url. + */ + want: ` + + + + + + + +`, + }, + { + name: "hrefTestOne", + input: ` + test 1 + test 2 + test 3 + test 4 + + test 5 + test 6 + + test 7 + test 8 +`, + /* Adjustement from https://github.com/darylldoyle/svg-sanitizer base + Diff: + --- Expected + +++ Actual + @@ -5,6 +5,4 @@ + test 4 + - + test 5 + test 6 + - + test 7 + */ + want: ` + test 1 + test 2 + test 3 + test 4 + test 5 + test 6 + test 7 + test 8 +`, + }, + { + name: "hrefTestTwo", + input: ` + test 1 + test 2 + test 3 + test 4 + + test 5 + test 6 + + test 7 + test 8 +`, + /* Adjustement from https://github.com/darylldoyle/svg-sanitizer base + Diff: + --- Expected + +++ Actual + @@ -5,6 +5,4 @@ + test 4 + - + test 5 + test 6 + - + test 7 + */ + want: ` + test 1 + test 2 + test 3 + test 4 + test 5 + test 6 + test 7 + test 8 +`, + }, + { + name: "svgTestOne", + input: ` + + + + + + + + + +shouldn't be here + + +`, + /* Adjustement from https://github.com/darylldoyle/svg-sanitizer base + Diff: + --- Expected + +++ Actual + @@ -1,3 +1,2 @@ + - + - + + + + @@ -8,4 +7,2 @@ + + - + - + + */ + want: ` + + + + + + + +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := SanitizeSVG(bytes.NewBufferString(tt.input)) + assert.Equal(t, tt.want, out, "The sanitized svg is not equal") + }) + } +}