Changed: DB Params
This commit is contained in:
199
templ/safehtml/style.go
Normal file
199
templ/safehtml/style.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Adapted from https://raw.githubusercontent.com/google/safehtml/3c4cd5b5d8c9a6c5882fba099979e9f50b65c876/style.go
|
||||
|
||||
// Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
package safehtml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SanitizeCSS attempts to sanitize CSS properties.
|
||||
func SanitizeCSS(property, value string) (string, string) {
|
||||
property = SanitizeCSSProperty(property)
|
||||
if property == InnocuousPropertyName {
|
||||
return InnocuousPropertyName, InnocuousPropertyValue
|
||||
}
|
||||
return property, SanitizeCSSValue(property, value)
|
||||
}
|
||||
|
||||
func SanitizeCSSValue(property, value string) string {
|
||||
if sanitizer, ok := cssPropertyNameToValueSanitizer[property]; ok {
|
||||
return sanitizer(value)
|
||||
}
|
||||
return sanitizeRegular(value)
|
||||
}
|
||||
|
||||
func SanitizeCSSProperty(property string) string {
|
||||
if !identifierPattern.MatchString(property) {
|
||||
return InnocuousPropertyName
|
||||
}
|
||||
return strings.ToLower(property)
|
||||
}
|
||||
|
||||
// identifierPattern matches a subset of valid <ident-token> values defined in
|
||||
// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram. This pattern matches all generic family name
|
||||
// keywords defined in https://drafts.csswg.org/css-fonts-3/#family-name-value.
|
||||
var identifierPattern = regexp.MustCompile(`^[-a-zA-Z]+$`)
|
||||
|
||||
var cssPropertyNameToValueSanitizer = map[string]func(string) string{
|
||||
"background-image": sanitizeBackgroundImage,
|
||||
"font-family": sanitizeFontFamily,
|
||||
"display": sanitizeEnum,
|
||||
"background-color": sanitizeRegular,
|
||||
"background-position": sanitizeRegular,
|
||||
"background-repeat": sanitizeRegular,
|
||||
"background-size": sanitizeRegular,
|
||||
"color": sanitizeRegular,
|
||||
"height": sanitizeRegular,
|
||||
"width": sanitizeRegular,
|
||||
"left": sanitizeRegular,
|
||||
"right": sanitizeRegular,
|
||||
"top": sanitizeRegular,
|
||||
"bottom": sanitizeRegular,
|
||||
"font-weight": sanitizeRegular,
|
||||
"padding": sanitizeRegular,
|
||||
"z-index": sanitizeRegular,
|
||||
}
|
||||
|
||||
var validURLPrefixes = []string{
|
||||
`url("`,
|
||||
`url('`,
|
||||
`url(`,
|
||||
}
|
||||
|
||||
var validURLSuffixes = []string{
|
||||
`")`,
|
||||
`')`,
|
||||
`)`,
|
||||
}
|
||||
|
||||
func sanitizeBackgroundImage(v string) string {
|
||||
// Check for <> as per https://github.com/google/safehtml/blob/be23134998433fcf0135dda53593fc8f8bf4df7c/style.go#L87C2-L89C3
|
||||
if strings.ContainsAny(v, "<>") {
|
||||
return InnocuousPropertyValue
|
||||
}
|
||||
for _, u := range strings.Split(v, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
var found bool
|
||||
for i, prefix := range validURLPrefixes {
|
||||
if strings.HasPrefix(u, prefix) && strings.HasSuffix(u, validURLSuffixes[i]) {
|
||||
found = true
|
||||
u = strings.TrimPrefix(u, validURLPrefixes[i])
|
||||
u = strings.TrimSuffix(u, validURLSuffixes[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found || !urlIsSafe(u) {
|
||||
return InnocuousPropertyValue
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func urlIsSafe(s string) bool {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if u.IsAbs() {
|
||||
if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") || strings.EqualFold(u.Scheme, "mailto") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var genericFontFamilyName = regexp.MustCompile(`^[a-zA-Z][- a-zA-Z]+$`)
|
||||
|
||||
func sanitizeFontFamily(s string) string {
|
||||
for _, f := range strings.Split(s, ",") {
|
||||
f = strings.TrimSpace(f)
|
||||
if strings.HasPrefix(f, `"`) {
|
||||
if !strings.HasSuffix(f, `"`) {
|
||||
return InnocuousPropertyValue
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !genericFontFamilyName.MatchString(f) {
|
||||
return InnocuousPropertyValue
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func sanitizeEnum(s string) string {
|
||||
if !safeEnumPropertyValuePattern.MatchString(s) {
|
||||
return InnocuousPropertyValue
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func sanitizeRegular(s string) string {
|
||||
if !safeRegularPropertyValuePattern.MatchString(s) {
|
||||
return InnocuousPropertyValue
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// InnocuousPropertyName is an innocuous property generated by a sanitizer when its input is unsafe.
|
||||
const InnocuousPropertyName = "zTemplUnsafeCSSPropertyName"
|
||||
|
||||
// InnocuousPropertyValue is an innocuous property generated by a sanitizer when its input is unsafe.
|
||||
const InnocuousPropertyValue = "zTemplUnsafeCSSPropertyValue"
|
||||
|
||||
// safeRegularPropertyValuePattern matches strings that are safe to use as property values.
|
||||
// Specifically, it matches string where every '*' or '/' is followed by end-of-text or a safe rune
|
||||
// (i.e. alphanumerics or runes in the set [+-.!#%_ \t]). This regex ensures that the following
|
||||
// are disallowed:
|
||||
// - "/*" and "*/", which are CSS comment markers.
|
||||
// - "//", even though this is not a comment marker in the CSS specification. Disallowing
|
||||
// this string minimizes the chance that browser peculiarities or parsing bugs will allow
|
||||
// sanitization to be bypassed.
|
||||
// - '(' and ')', which can be used to call functions.
|
||||
// - ',', since it can be used to inject extra values into a property.
|
||||
// - Runes which could be matched on CSS error recovery of a previously malformed token, such as '@'
|
||||
// and ':'. See http://www.w3.org/TR/css3-syntax/#error-handling.
|
||||
var safeRegularPropertyValuePattern = regexp.MustCompile(`^(?:[*/]?(?:[0-9a-zA-Z+-.!#%_ \t]|$))*$`)
|
||||
|
||||
// safeEnumPropertyValuePattern matches strings that are safe to use as enumerated property values.
|
||||
// Specifically, it matches strings that contain only alphabetic and '-' runes.
|
||||
var safeEnumPropertyValuePattern = regexp.MustCompile(`^[a-zA-Z-]*$`)
|
||||
|
||||
// SanitizeStyleValue escapes s so that it is safe to put between "" to form a CSS <string-token>.
|
||||
// See syntax at https://www.w3.org/TR/css-syntax-3/#string-token-diagram.
|
||||
//
|
||||
// On top of the escape sequences required in <string-token>, this function also escapes
|
||||
// control runes to minimize the risk of these runes triggering browser-specific bugs.
|
||||
// Taken from cssEscapeString in safehtml package.
|
||||
func SanitizeStyleValue(s string) string {
|
||||
var b bytes.Buffer
|
||||
b.Grow(len(s))
|
||||
for _, c := range s {
|
||||
switch {
|
||||
case c == '\u0000':
|
||||
// Replace the NULL byte according to https://www.w3.org/TR/css-syntax-3/#input-preprocessing.
|
||||
// We take this extra precaution in case the user agent fails to handle NULL properly.
|
||||
b.WriteString("\uFFFD")
|
||||
case c == '<', // Prevents breaking out of a style element with `</style>`. Escape this in case the Style user forgets to.
|
||||
c == '"', c == '\\', // Must be CSS-escaped in <string-token>. U+000A line feed is handled in the next case.
|
||||
c <= '\u001F', c == '\u007F', // C0 control codes
|
||||
c >= '\u0080' && c <= '\u009F', // C1 control codes
|
||||
c == '\u2028', c == '\u2029': // Unicode newline characters
|
||||
// See CSS escape sequence syntax at https://www.w3.org/TR/css-syntax-3/#escape-diagram.
|
||||
fmt.Fprintf(&b, "\\%06X", c)
|
||||
default:
|
||||
b.WriteRune(c)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
362
templ/safehtml/style_test.go
Normal file
362
templ/safehtml/style_test.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package safehtml
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSanitizeCSS(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputProperty string
|
||||
expectedProperty string
|
||||
inputValue string
|
||||
expectedValue string
|
||||
}{
|
||||
{
|
||||
name: "directions are allowed",
|
||||
inputProperty: "dir",
|
||||
expectedProperty: "dir",
|
||||
inputValue: "ltr",
|
||||
expectedValue: "ltr",
|
||||
},
|
||||
{
|
||||
name: "border-left allowed",
|
||||
inputProperty: "border-left",
|
||||
expectedProperty: "border-left",
|
||||
inputValue: "0",
|
||||
expectedValue: "0",
|
||||
},
|
||||
{
|
||||
name: "border can contain multiple values",
|
||||
inputProperty: "border",
|
||||
expectedProperty: "border",
|
||||
inputValue: `1 1 1 1`,
|
||||
expectedValue: `1 1 1 1`,
|
||||
},
|
||||
{
|
||||
name: "properties are case corrected",
|
||||
inputProperty: "Border",
|
||||
expectedProperty: "border",
|
||||
inputValue: `1 1 1 1`,
|
||||
expectedValue: `1 1 1 1`,
|
||||
},
|
||||
{
|
||||
name: "expressions are not allowed",
|
||||
inputProperty: "width",
|
||||
expectedProperty: "width",
|
||||
inputValue: `expression(alert(1337))`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "font-family standard values are allowed",
|
||||
inputProperty: "font-family",
|
||||
expectedProperty: "font-family",
|
||||
inputValue: `sans-serif`,
|
||||
expectedValue: `sans-serif`,
|
||||
},
|
||||
{
|
||||
name: "font-family values with spaces are allowed",
|
||||
inputProperty: "font-family",
|
||||
expectedProperty: "font-family",
|
||||
inputValue: `Akzidenz Grotesk`,
|
||||
expectedValue: `Akzidenz Grotesk`,
|
||||
},
|
||||
{
|
||||
name: "font-family multiple standard values are allowed",
|
||||
inputProperty: "font-family",
|
||||
expectedProperty: "font-family",
|
||||
inputValue: `sans-serif, monospaced`,
|
||||
expectedValue: `sans-serif, monospaced`,
|
||||
},
|
||||
{
|
||||
name: "font-family multiple quoted and non-quoted values are allowed",
|
||||
inputProperty: "font-family",
|
||||
expectedProperty: "font-family",
|
||||
inputValue: `"Georgia", monospaced, sans-serif`,
|
||||
expectedValue: `"Georgia", monospaced, sans-serif`,
|
||||
},
|
||||
{
|
||||
name: "font-family Chinese names are allowed",
|
||||
inputProperty: "font-family",
|
||||
expectedProperty: "font-family",
|
||||
inputValue: `"中易宋体", monospaced`,
|
||||
expectedValue: `"中易宋体", monospaced`,
|
||||
},
|
||||
{
|
||||
name: "font-family quoted values must be terminated",
|
||||
inputProperty: "font-family",
|
||||
expectedProperty: "font-family",
|
||||
inputValue: `"quotes`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "font-family non standard names are not allowed",
|
||||
inputProperty: "font-family",
|
||||
expectedProperty: "font-family",
|
||||
inputValue: `foo@bar`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "obfuscated values are not allowed",
|
||||
inputProperty: "width",
|
||||
expectedProperty: "width",
|
||||
inputValue: ` e\\78preS\x00Sio/**/n(alert(1337))`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "moz binding blocked",
|
||||
inputProperty: "-moz-binding(alert(1337))",
|
||||
expectedProperty: InnocuousPropertyName,
|
||||
inputValue: `something`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "obfuscated moz-binding blocked",
|
||||
inputProperty: " -mo\\7a-B\x00I/**/nding(alert(1337))",
|
||||
expectedProperty: InnocuousPropertyName,
|
||||
inputValue: `something`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "angle brackets in property value",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url(/img?name=O'Reilly Animal(1)<2>.png)`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "angle brackets in quoted property value",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("/img?name=O'Reilly Animal(1)<2>.png")`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "background",
|
||||
inputProperty: "background",
|
||||
expectedProperty: "background",
|
||||
inputValue: "url(/img?name=O%27Reilly%20Animal%281%29%3c2%3e.png)",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "background-image JS URL",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url(javascript:alert(1337))`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "background-image VBScript URL",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url(vbscript:alert(1337))`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "background-image absolute FTP URL",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("ftp://safe.example.com/img.png")`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "background-image invalid URL",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("` + string([]byte{0x7f}) + `")`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "background-image invalid prefix",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `/img.png")`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "background-image invalid suffix",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("/img.png`,
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "background-image safe URL",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("/img.png")`,
|
||||
expectedValue: `url("/img.png")`,
|
||||
},
|
||||
{
|
||||
name: "background-image safe URL - two slashes",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("//img.png")`,
|
||||
expectedValue: `url("//img.png")`,
|
||||
},
|
||||
{
|
||||
name: "background-image safe HTTP URL",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("http://safe.example.com/img.png")`,
|
||||
expectedValue: `url("http://safe.example.com/img.png")`,
|
||||
},
|
||||
{
|
||||
name: "background-image safe mailto URL",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("mailto:foo@bar.foo")`,
|
||||
expectedValue: `url("mailto:foo@bar.foo")`,
|
||||
},
|
||||
{
|
||||
name: "background-image multiple URLs",
|
||||
inputProperty: "background-image",
|
||||
expectedProperty: "background-image",
|
||||
inputValue: `url("http://safe.example.com/img.png"), url("https://safe.example.com/other.png")`,
|
||||
expectedValue: `url("http://safe.example.com/img.png"), url("https://safe.example.com/other.png")`,
|
||||
},
|
||||
{
|
||||
name: "-webkit-text-stroke-color safe webkit",
|
||||
inputProperty: "-webkit-text-stroke-color",
|
||||
expectedProperty: "-webkit-text-stroke-color",
|
||||
inputValue: `#000`,
|
||||
expectedValue: `#000`,
|
||||
},
|
||||
{
|
||||
name: "escape attempt property name",
|
||||
inputProperty: "</style><script>alert('hello')</script><style>",
|
||||
expectedProperty: InnocuousPropertyName,
|
||||
inputValue: "test",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "escape attempt property value",
|
||||
inputProperty: "bottom",
|
||||
expectedProperty: "bottom",
|
||||
inputValue: "</style><script>alert('hello')</script><style>",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "encoded protocol is blocked",
|
||||
inputProperty: "bottom",
|
||||
expectedProperty: "bottom",
|
||||
inputValue: "javascript\\3a alert(1337)",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "angle brackets 1",
|
||||
inputProperty: "bottom",
|
||||
expectedProperty: "bottom",
|
||||
inputValue: "x<",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "angle brackets 2",
|
||||
inputProperty: "bottom",
|
||||
expectedProperty: "bottom",
|
||||
inputValue: "x>",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "contains colon",
|
||||
inputProperty: "bottom",
|
||||
expectedProperty: "bottom",
|
||||
inputValue: ":",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "contains semicolon",
|
||||
inputProperty: "bottom",
|
||||
expectedProperty: "bottom",
|
||||
inputValue: ";",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "comments are removed #1",
|
||||
inputProperty: "background-repeat",
|
||||
expectedProperty: "background-repeat",
|
||||
inputValue: "// removed",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "comments are removed #2",
|
||||
inputProperty: "background-repeat",
|
||||
expectedProperty: "background-repeat",
|
||||
inputValue: "/* removed",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "comments are removed #3",
|
||||
inputProperty: "background-repeat",
|
||||
expectedProperty: "background-repeat",
|
||||
inputValue: "/* removed",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "comments are sanitized mid value #1",
|
||||
inputProperty: "background-repeat",
|
||||
expectedProperty: "background-repeat",
|
||||
inputValue: "repeat-none // removed",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "comments are sanitized mid value #2",
|
||||
inputProperty: "background-repeat",
|
||||
expectedProperty: "background-repeat",
|
||||
inputValue: "repeat-none /* removed",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "comments are sanitized mid value #3",
|
||||
inputProperty: "background-repeat",
|
||||
expectedProperty: "background-repeat",
|
||||
inputValue: "repeat-none /* removed",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "bad characters in value",
|
||||
inputProperty: "background-repeat",
|
||||
expectedProperty: "background-repeat",
|
||||
inputValue: "This&is$bad",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "enum values must be valid",
|
||||
inputProperty: "display",
|
||||
expectedProperty: "display",
|
||||
inputValue: "badValue123",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "values cannot contain newlines at start",
|
||||
inputProperty: "display",
|
||||
expectedProperty: "display",
|
||||
inputValue: "\ntest",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "values cannot contain newlines at end",
|
||||
inputProperty: "display",
|
||||
expectedProperty: "display",
|
||||
inputValue: "test\n",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
{
|
||||
name: "some symbols are allowed",
|
||||
inputProperty: "display",
|
||||
expectedProperty: "display",
|
||||
inputValue: "*+/-.!#%_ \t",
|
||||
expectedValue: InnocuousPropertyValue,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actualProperty, actualValue := SanitizeCSS(tt.inputProperty, tt.inputValue)
|
||||
if actualProperty != tt.expectedProperty {
|
||||
t.Errorf("%s: mismatched property - expected %q, actual %q", tt.name, tt.expectedProperty, actualProperty)
|
||||
}
|
||||
if actualValue != tt.expectedValue {
|
||||
t.Errorf("%s: mismatched value - expected %q, actual %q", tt.name, tt.expectedValue, actualValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user