style.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. // Adapted from https://raw.githubusercontent.com/google/safehtml/3c4cd5b5d8c9a6c5882fba099979e9f50b65c876/style.go
  2. // Copyright (c) 2017 The Go Authors. All rights reserved.
  3. //
  4. // Use of this source code is governed by a BSD-style
  5. // license that can be found in the LICENSE file or at
  6. // https://developers.google.com/open-source/licenses/bsd
  7. package safehtml
  8. import (
  9. "net/url"
  10. "regexp"
  11. "strings"
  12. )
  13. // SanitizeCSS attempts to sanitize CSS properties.
  14. func SanitizeCSS(property, value string) (string, string) {
  15. property = SanitizeCSSProperty(property)
  16. if property == InnocuousPropertyName {
  17. return InnocuousPropertyName, InnocuousPropertyValue
  18. }
  19. return property, SanitizeCSSValue(property, value)
  20. }
  21. func SanitizeCSSValue(property, value string) string {
  22. if sanitizer, ok := cssPropertyNameToValueSanitizer[property]; ok {
  23. return sanitizer(value)
  24. }
  25. return sanitizeRegular(value)
  26. }
  27. func SanitizeCSSProperty(property string) string {
  28. if !identifierPattern.MatchString(property) {
  29. return InnocuousPropertyName
  30. }
  31. return strings.ToLower(property)
  32. }
  33. // identifierPattern matches a subset of valid <ident-token> values defined in
  34. // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram. This pattern matches all generic family name
  35. // keywords defined in https://drafts.csswg.org/css-fonts-3/#family-name-value.
  36. var identifierPattern = regexp.MustCompile(`^[-a-zA-Z]+$`)
  37. var cssPropertyNameToValueSanitizer = map[string]func(string) string{
  38. "background-image": sanitizeBackgroundImage,
  39. "font-family": sanitizeFontFamily,
  40. "display": sanitizeEnum,
  41. "background-color": sanitizeRegular,
  42. "background-position": sanitizeRegular,
  43. "background-repeat": sanitizeRegular,
  44. "background-size": sanitizeRegular,
  45. "color": sanitizeRegular,
  46. "height": sanitizeRegular,
  47. "width": sanitizeRegular,
  48. "left": sanitizeRegular,
  49. "right": sanitizeRegular,
  50. "top": sanitizeRegular,
  51. "bottom": sanitizeRegular,
  52. "font-weight": sanitizeRegular,
  53. "padding": sanitizeRegular,
  54. "z-index": sanitizeRegular,
  55. }
  56. var validURLPrefixes = []string{
  57. `url("`,
  58. `url('`,
  59. `url(`,
  60. }
  61. var validURLSuffixes = []string{
  62. `")`,
  63. `')`,
  64. `)`,
  65. }
  66. func sanitizeBackgroundImage(v string) string {
  67. // Check for <> as per https://github.com/google/safehtml/blob/be23134998433fcf0135dda53593fc8f8bf4df7c/style.go#L87C2-L89C3
  68. if strings.ContainsAny(v, "<>") {
  69. return InnocuousPropertyValue
  70. }
  71. for _, u := range strings.Split(v, ",") {
  72. u = strings.TrimSpace(u)
  73. var found bool
  74. for i, prefix := range validURLPrefixes {
  75. if strings.HasPrefix(u, prefix) && strings.HasSuffix(u, validURLSuffixes[i]) {
  76. found = true
  77. u = strings.TrimPrefix(u, validURLPrefixes[i])
  78. u = strings.TrimSuffix(u, validURLSuffixes[i])
  79. break
  80. }
  81. }
  82. if !found || !urlIsSafe(u) {
  83. return InnocuousPropertyValue
  84. }
  85. }
  86. return v
  87. }
  88. func urlIsSafe(s string) bool {
  89. u, err := url.Parse(s)
  90. if err != nil {
  91. return false
  92. }
  93. if u.IsAbs() {
  94. if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") || strings.EqualFold(u.Scheme, "mailto") {
  95. return true
  96. }
  97. return false
  98. }
  99. return true
  100. }
  101. var genericFontFamilyName = regexp.MustCompile(`^[a-zA-Z][- a-zA-Z]+$`)
  102. func sanitizeFontFamily(s string) string {
  103. for _, f := range strings.Split(s, ",") {
  104. f = strings.TrimSpace(f)
  105. if strings.HasPrefix(f, `"`) {
  106. if !strings.HasSuffix(f, `"`) {
  107. return InnocuousPropertyValue
  108. }
  109. continue
  110. }
  111. if !genericFontFamilyName.MatchString(f) {
  112. return InnocuousPropertyValue
  113. }
  114. }
  115. return s
  116. }
  117. func sanitizeEnum(s string) string {
  118. if !safeEnumPropertyValuePattern.MatchString(s) {
  119. return InnocuousPropertyValue
  120. }
  121. return s
  122. }
  123. func sanitizeRegular(s string) string {
  124. if !safeRegularPropertyValuePattern.MatchString(s) {
  125. return InnocuousPropertyValue
  126. }
  127. return s
  128. }
  129. // InnocuousPropertyName is an innocuous property generated by a sanitizer when its input is unsafe.
  130. const InnocuousPropertyName = "zTemplUnsafeCSSPropertyName"
  131. // InnocuousPropertyValue is an innocuous property generated by a sanitizer when its input is unsafe.
  132. const InnocuousPropertyValue = "zTemplUnsafeCSSPropertyValue"
  133. // safeRegularPropertyValuePattern matches strings that are safe to use as property values.
  134. // Specifically, it matches string where every '*' or '/' is followed by end-of-text or a safe rune
  135. // (i.e. alphanumerics or runes in the set [+-.!#%_ \t]). This regex ensures that the following
  136. // are disallowed:
  137. // - "/*" and "*/", which are CSS comment markers.
  138. // - "//", even though this is not a comment marker in the CSS specification. Disallowing
  139. // this string minimizes the chance that browser peculiarities or parsing bugs will allow
  140. // sanitization to be bypassed.
  141. // - '(' and ')', which can be used to call functions.
  142. // - ',', since it can be used to inject extra values into a property.
  143. // - Runes which could be matched on CSS error recovery of a previously malformed token, such as '@'
  144. // and ':'. See http://www.w3.org/TR/css3-syntax/#error-handling.
  145. var safeRegularPropertyValuePattern = regexp.MustCompile(`^(?:[*/]?(?:[0-9a-zA-Z+-.!#%_ \t]|$))*$`)
  146. // safeEnumPropertyValuePattern matches strings that are safe to use as enumerated property values.
  147. // Specifically, it matches strings that contain only alphabetic and '-' runes.
  148. var safeEnumPropertyValuePattern = regexp.MustCompile(`^[a-zA-Z-]*$`)