Repository: microcosm-cc/bluemonday (go)

Issue:
Add crossorigin
Hi David,

Would you accept a PR which adds option to force `crossorigin="anonymous"` (https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin) on audio, img, link, script, and video elements?



Pull request description:
Closed #107

Patch:
diff --git a/policy.go b/policy.go
index 739d302..9c7e662 100644
--- a/policy.go
+++ b/policy.go
@@ -69,6 +69,9 @@ type Policy struct {
 	// Will skip for href="/foo" or href="foo"
 	requireNoReferrerFullyQualifiedLinks bool
 
+	// When true, add crossorigin="anonymous" to HTML audio, img, link, script, and video tags
+	requireCrossOriginAnonymous bool
+
 	// When true add target="_blank" to fully qualified links
 	// Will add for href="http://foo"
 	// Will skip for href="/foo" or href="foo"
@@ -433,24 +436,24 @@ func (spb *stylePolicyBuilder) OnElements(elements ...string) *Policy {
 // and return the updated policy
 func (spb *stylePolicyBuilder) OnElementsMatching(regex *regexp.Regexp) *Policy {
 
-		for _, attr := range spb.propertyNames {
+	for _, attr := range spb.propertyNames {
 
-			if _, ok := spb.p.elsMatchingAndStyles[regex]; !ok {
-				spb.p.elsMatchingAndStyles[regex] = make(map[string]stylePolicy)
-			}
+		if _, ok := spb.p.elsMatchingAndStyles[regex]; !ok {
+			spb.p.elsMatchingAndStyles[regex] = make(map[string]stylePolicy)
+		}
 
-			sp := stylePolicy{}
-			if spb.handler != nil {
-				sp.handler = spb.handler
-			} else if len(spb.enum) > 0 {
-				sp.enum = spb.enum
-			} else if spb.regexp != nil {
-				sp.regexp = spb.regexp
-			} else {
-				sp.handler = getDefaultHandler(attr)
-			}
-			spb.p.elsMatchingAndStyles[regex][attr] = sp
+		sp := stylePolicy{}
+		if spb.handler != nil {
+			sp.handler = spb.handler
+		} else if len(spb.enum) > 0 {
+			sp.enum = spb.enum
+		} else if spb.regexp != nil {
+			sp.regexp = spb.regexp
+		} else {
+			sp.handler = getDefaultHandler(attr)
 		}
+		spb.p.elsMatchingAndStyles[regex][attr] = sp
+	}
 
 	return spb.p
 }
@@ -558,6 +561,16 @@ func (p *Policy) RequireNoReferrerOnFullyQualifiedLinks(require bool) *Policy {
 	return p
 }
 
+// RequireCrossOriginAnonymous will result in all audio, img, link, script, and
+// video tags having a crossorigin="anonymous" added to them if one does not
+// already exist
+func (p *Policy) RequireCrossOriginAnonymous(require bool) *Policy {
+
+	p.requireCrossOriginAnonymous = require
+
+	return p
+}
+
 // AddTargetBlankToFullyQualifiedLinks will result in all a, area and link tags
 // that point to a non-local destination (i.e. starts with a protocol and has a
 // host) having a target="_blank" added to them if one does not already exist
diff --git a/sanitize.go b/sanitize.go
index 7386b8e..d356fbd 100644
--- a/sanitize.go
+++ b/sanitize.go
@@ -721,6 +721,26 @@ func (p *Policy) sanitizeAttrs(
 		}
 	}
 
+	if p.requireCrossOriginAnonymous && len(cleanAttrs) > 0 {
+		switch elementName {
+		case "audio", "img", "link", "script", "video":
+			var crossOriginFound bool
+			for _, htmlAttr := range cleanAttrs {
+				if htmlAttr.Key == "crossorigin" {
+					crossOriginFound = true
+					htmlAttr.Val = "anonymous"
+				}
+			}
+
+			if !crossOriginFound {
+				crossOrigin := html.Attribute{}
+				crossOrigin.Key = "crossorigin"
+				crossOrigin.Val = "anonymous"
+				cleanAttrs = append(cleanAttrs, crossOrigin)
+			}
+		}
+	}
+
 	return cleanAttrs
 }
 


Test patch:
diff --git a/policy_test.go b/policy_test.go
index b352c30..da38734 100644
--- a/policy_test.go
+++ b/policy_test.go
@@ -72,7 +72,7 @@ func TestAllowElementsMatching(t *testing.T) {
 			policyFn: func(policy *Policy) {
 				policy.AllowElementsMatching(regexp.MustCompile(`^my-element-`))
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test" my-attr="test"/>
 							<my-element-demo-two data-test="test"/>
 							<not-my-element-demo-one data-test="test"/>
@@ -86,7 +86,7 @@ func TestAllowElementsMatching(t *testing.T) {
 			policyFn: func(policy *Policy) {
 				policy.AllowElementsMatching(regexp.MustCompile(`^my-element-`))
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test"></my-element-demo-one>
 							<my-element-demo-two data-test="test"></my-element-demo-two>
 							<not-my-element-demo-one data-test="test"></not-my-element-demo-one>
@@ -96,12 +96,12 @@ func TestAllowElementsMatching(t *testing.T) {
 							<my-element-demo-two data-test="test"></my-element-demo-two>
 							
 						</div>`,
-		},"Self closing tags with regex prefix and custom attr should strip any that do not match": {
+		}, "Self closing tags with regex prefix and custom attr should strip any that do not match": {
 			policyFn: func(policy *Policy) {
 				policy.AllowElementsMatching(regexp.MustCompile(`^my-element-`))
 				policy.AllowElements("not-my-element-demo-one")
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test" my-attr="test"/>
 							<my-element-demo-two data-test="test"/>
 							<not-my-element-demo-one data-test="test"/>
@@ -117,7 +117,7 @@ func TestAllowElementsMatching(t *testing.T) {
 	for name, test := range tests {
 		policy := NewPolicy().AllowElements("div")
 		policy.AllowDataAttributes()
-		if test.policyFn != nil{
+		if test.policyFn != nil {
 			test.policyFn(policy)
 		}
 		out := policy.Sanitize(test.in)
@@ -133,7 +133,7 @@ func TestAllowElementsMatching(t *testing.T) {
 	}
 }
 
-func TestAttrOnElementMatching(t *testing.T){
+func TestAttrOnElementMatching(t *testing.T) {
 	tests := map[string]struct {
 		policyFn func(policy *Policy)
 		in       string
@@ -143,7 +143,7 @@ func TestAttrOnElementMatching(t *testing.T){
 			policyFn: func(policy *Policy) {
 				policy.AllowAttrs("my-attr").OnElementsMatching(regexp.MustCompile(`^my-element-`))
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test" my-attr="test"/>
 							<my-element-demo-two data-test="test" other-attr="test"/>
 							<not-my-element-demo-one data-test="test"/>
@@ -157,7 +157,7 @@ func TestAttrOnElementMatching(t *testing.T){
 			policyFn: func(policy *Policy) {
 				policy.AllowAttrs("my-attr").OnElementsMatching(regexp.MustCompile(`^my-element-`))
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test" my-attr="test" other-attr="test"></my-element-demo-one>
 							<my-element-demo-two data-test="test" other-attr="test"></my-element-demo-two>
 							<not-my-element-demo-one data-test="test" other-attr="test"></not-my-element-demo-one>
@@ -167,14 +167,14 @@ func TestAttrOnElementMatching(t *testing.T){
 							<my-element-demo-two data-test="test"></my-element-demo-two>
 							
 						</div>`,
-		},"Specific element rule defined should override matching rules": {
+		}, "Specific element rule defined should override matching rules": {
 			policyFn: func(policy *Policy) {
 				// specific element rule
 				policy.AllowAttrs("my-other-attr").OnElements("my-element-demo-one")
 				// matched rule takes lower precedence
 				policy.AllowAttrs("my-attr").OnElementsMatching(regexp.MustCompile(`^my-element-`))
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test" my-attr="test" my-other-attr="test"/>
 							<my-element-demo-two data-test="test" my-attr="test" my-other-attr="test"/>
 							<not-my-element-demo-one data-test="test"/>
@@ -190,7 +190,7 @@ func TestAttrOnElementMatching(t *testing.T){
 	for name, test := range tests {
 		policy := NewPolicy().AllowElements("div")
 		policy.AllowDataAttributes()
-		if test.policyFn != nil{
+		if test.policyFn != nil {
 			test.policyFn(policy)
 		}
 		out := policy.Sanitize(test.in)
@@ -206,7 +206,7 @@ func TestAttrOnElementMatching(t *testing.T){
 	}
 }
 
-func TestStyleOnElementMatching(t *testing.T){
+func TestStyleOnElementMatching(t *testing.T) {
 	tests := map[string]struct {
 		policyFn func(policy *Policy)
 		in       string
@@ -216,12 +216,12 @@ func TestStyleOnElementMatching(t *testing.T){
 			policyFn: func(policy *Policy) {
 				policy.AllowAttrs("style").
 					OnElementsMatching(regexp.MustCompile(`^my-element-`))
-				policy.AllowStyles("color","mystyle").
+				policy.AllowStyles("color", "mystyle").
 					MatchingHandler(func(s string) bool {
-					return true
-				}).OnElementsMatching(regexp.MustCompile(`^my-element-`))
+						return true
+					}).OnElementsMatching(regexp.MustCompile(`^my-element-`))
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test" style="color:#ffffff;mystyle:test;other:value"/>
 							<my-element-demo-two data-test="test" other-attr="test" style="other:value"/>
 							<not-my-element-demo-one data-test="test"/>
@@ -235,12 +235,12 @@ func TestStyleOnElementMatching(t *testing.T){
 			policyFn: func(policy *Policy) {
 				policy.AllowAttrs("style").
 					OnElementsMatching(regexp.MustCompile(`^my-element-`))
-				policy.AllowStyles("color","mystyle").
+				policy.AllowStyles("color", "mystyle").
 					MatchingHandler(func(s string) bool {
 						return true
 					}).OnElementsMatching(regexp.MustCompile(`^my-element-`))
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test" style="color:#ffffff;mystyle:test;other:value"></my-element-demo-one>
 							<my-element-demo-two data-test="test" other-attr="test" style="other:value"></my-element-demo-two>
 							<not-my-element-demo-one data-test="test" other-attr="test"></not-my-element-demo-one>
@@ -250,23 +250,23 @@ func TestStyleOnElementMatching(t *testing.T){
 							<my-element-demo-two data-test="test"></my-element-demo-two>
 							
 						</div>`,
-		},"Specific element rule defined should override matching rules": {
+		}, "Specific element rule defined should override matching rules": {
 			policyFn: func(policy *Policy) {
 				policy.AllowAttrs("style").
 					OnElements("my-element-demo-one")
-				policy.AllowStyles("color","mystyle").
+				policy.AllowStyles("color", "mystyle").
 					MatchingHandler(func(s string) bool {
 						return true
 					}).OnElements("my-element-demo-one")
 
 				policy.AllowAttrs("style").
 					OnElementsMatching(regexp.MustCompile(`^my-element-`))
-				policy.AllowStyles("color","customstyle").
+				policy.AllowStyles("color", "customstyle").
 					MatchingHandler(func(s string) bool {
 						return true
 					}).OnElementsMatching(regexp.MustCompile(`^my-element-`))
 			},
-			in:       `<div>
+			in: `<div>
 							<my-element-demo-one data-test="test" style="color:#ffffff;mystyle:test;other:value"/>
 							<my-element-demo-two data-test="test" style="color:#ffffff;mystyle:test;customstyle:value"/>
 							<not-my-element-demo-one data-test="test"/>
@@ -282,7 +282,7 @@ func TestStyleOnElementMatching(t *testing.T){
 	for name, test := range tests {
 		policy := NewPolicy().AllowElements("div")
 		policy.AllowDataAttributes()
-		if test.policyFn != nil{
+		if test.policyFn != nil {
 			test.policyFn(policy)
 		}
 		out := policy.Sanitize(test.in)
@@ -296,4 +296,4 @@ func TestStyleOnElementMatching(t *testing.T){
 			)
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/sanitize_test.go b/sanitize_test.go
index 77a28a6..708bacd 100644
--- a/sanitize_test.go
+++ b/sanitize_test.go
@@ -1679,6 +1679,47 @@ func TestIssue85NoReferrer(t *testing.T) {
 	wg.Wait()
 }
 
+func TestIssue107(t *testing.T) {
+	p := UGCPolicy()
+	p.RequireCrossOriginAnonymous(true)
+
+	tests := []test{
+		{
+			in:       `<img src="/path" />`,
+			expected: `<img src="/path" crossorigin="anonymous"/>`,
+		},
+		{
+			in:       `<img src="/path" crossorigin="use-credentials"/>`,
+			expected: `<img src="/path" crossorigin="anonymous"/>`,
+		},
+		{
+			in:       `<img src="/path" crossorigin=""/>`,
+			expected: `<img src="/path" crossorigin="anonymous"/>`,
+		},
+	}
+
+	// These tests are run concurrently to enable the race detector to pick up
+	// potential issues
+	wg := sync.WaitGroup{}
+	wg.Add(len(tests))
+	for ii, tt := range tests {
+		go func(ii int, tt test) {
+			out := p.Sanitize(tt.in)
+			if out != tt.expected {
+				t.Errorf(
+					"test %d failed;\ninput   : %s\noutput  : %s\nexpected: %s",
+					ii,
+					tt.in,
+					out,
+					tt.expected,
+				)
+			}
+			wg.Done()
+		}(ii, tt)
+	}
+	wg.Wait()
+}
+
 func TestSanitizedURL(t *testing.T) {
 	tests := []test{
 		{