Repository: microcosm-cc/bluemonday
Pull request #86 (2019-07-23 18:05:52)

Add Noreferrer API
Functionality to add rel="noreferrer" to a, area, and link elements.


Patch:
diff --git a/README.md b/README.md
index 0a61782..ac17b4c 100644
--- a/README.md
+++ b/README.md
@@ -272,6 +272,13 @@ Regardless of whether you have enabled parseable URLs, you can force all URLs to
 p.RequireNoFollowOnLinks(true)
 ```
 
+Similarly, you can force all URLs to have "noreferrer" in their rel attribute.
+```go
+// This applies to "a" "area" "link" elements that have a "href" attribute
+p.RequireNoReferrerOnLinks(true)
+```
+
+
 We provide a convenience method that applies all of the above, but you will still need to whitelist the linkable elements for the URL rules to be applied to:
 ```go
 p.AllowStandardURLs()
diff --git a/helpers.go b/helpers.go
index dfa5868..089fe9d 100644
--- a/helpers.go
+++ b/helpers.go
@@ -135,7 +135,7 @@ func (p *Policy) AllowStandardURLs() {
 	// Most common URL schemes only
 	p.AllowURLSchemes("mailto", "http", "https")
 
-	// For all anchors we will add rel="nofollow" if it does not already exist
+	// For linking elements we will add rel="nofollow" if it does not already exist
 	// This applies to "a" "area" "link"
 	p.RequireNoFollowOnLinks(true)
 }
diff --git a/policy.go b/policy.go
index 945da07..17d3974 100644
--- a/policy.go
+++ b/policy.go
@@ -53,14 +53,22 @@ type Policy struct {
 	// tag is replaced by a space character.
 	addSpaces bool
 
-	// When true, add rel="nofollow" to HTML anchors
+	// When true, add rel="nofollow" to HTML a, area, and link tags
 	requireNoFollow bool
 
-	// When true, add rel="nofollow" to HTML anchors
+	// When true, add rel="nofollow" to HTML a, area, and link tags
 	// Will add for href="http://foo"
 	// Will skip for href="/foo" or href="foo"
 	requireNoFollowFullyQualifiedLinks bool
 
+	// When true, add rel="noreferrer" to HTML a, area, and link tags
+	requireNoReferrer bool
+
+	// When true, add rel="noreferrer" to HTML a, area, and link tags
+	// Will add for href="http://foo"
+	// Will skip for href="/foo" or href="foo"
+	requireNoReferrerFullyQualifiedLinks bool
+
 	// When true add target="_blank" to fully qualified links
 	// Will add for href="http://foo"
 	// Will skip for href="/foo" or href="foo"
@@ -422,8 +430,8 @@ func (p *Policy) AllowElements(names ...string) *Policy {
 	return p
 }
 
-// RequireNoFollowOnLinks will result in all <a> tags having a rel="nofollow"
-// added to them if one does not already exist
+// RequireNoFollowOnLinks will result in all a, area, link tags having a
+// rel="nofollow"added to them if one does not already exist
 //
 // Note: This requires p.RequireParseableURLs(true) and will enable it.
 func (p *Policy) RequireNoFollowOnLinks(require bool) *Policy {
@@ -434,9 +442,10 @@ func (p *Policy) RequireNoFollowOnLinks(require bool) *Policy {
 	return p
 }
 
-// RequireNoFollowOnFullyQualifiedLinks will result in all <a> tags that point
-// to a non-local destination (i.e. starts with a protocol and has a host)
-// having a rel="nofollow" added to them if one does not already exist
+// RequireNoFollowOnFullyQualifiedLinks 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 rel="nofollow" added to them if one does not already
+// exist
 //
 // Note: This requires p.RequireParseableURLs(true) and will enable it.
 func (p *Policy) RequireNoFollowOnFullyQualifiedLinks(require bool) *Policy {
@@ -447,9 +456,35 @@ func (p *Policy) RequireNoFollowOnFullyQualifiedLinks(require bool) *Policy {
 	return p
 }
 
-// AddTargetBlankToFullyQualifiedLinks will result in all <a> 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
+// RequireNoReferrerOnLinks will result in all a, area, and link tags having a
+// rel="noreferrrer" added to them if one does not already exist
+//
+// Note: This requires p.RequireParseableURLs(true) and will enable it.
+func (p *Policy) RequireNoReferrerOnLinks(require bool) *Policy {
+
+	p.requireNoReferrer = require
+	p.requireParseableURLs = true
+
+	return p
+}
+
+// RequireNoReferrerOnFullyQualifiedLinks 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 rel="noreferrer" added to them if one does not already
+// exist
+//
+// Note: This requires p.RequireParseableURLs(true) and will enable it.
+func (p *Policy) RequireNoReferrerOnFullyQualifiedLinks(require bool) *Policy {
+
+	p.requireNoReferrerFullyQualifiedLinks = require
+	p.requireParseableURLs = true
+
+	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
 //
 // Note: This requires p.RequireParseableURLs(true) and will enable it.
 func (p *Policy) AddTargetBlankToFullyQualifiedLinks(require bool) *Policy {
diff --git a/sanitize.go b/sanitize.go
index 7cd4164..ab18308 100644
--- a/sanitize.go
+++ b/sanitize.go
@@ -378,6 +378,8 @@ func (p *Policy) sanitizeAttrs(
 
 		if (p.requireNoFollow ||
 			p.requireNoFollowFullyQualifiedLinks ||
+			p.requireNoReferrer ||
+			p.requireNoReferrerFullyQualifiedLinks ||
 			p.addTargetBlankToFullyQualifiedLinks) &&
 			len(cleanAttrs) > 0 {
 
@@ -405,12 +407,16 @@ func (p *Policy) sanitizeAttrs(
 				if hrefFound {
 					var (
 						noFollowFound    bool
+						noReferrerFound  bool
 						targetBlankFound bool
 					)
 
 					addNoFollow := (p.requireNoFollow ||
 						externalLink && p.requireNoFollowFullyQualifiedLinks)
 
+					addNoReferrer := (p.requireNoReferrer ||
+						externalLink && p.requireNoReferrerFullyQualifiedLinks)
+
 					addTargetBlank := (externalLink &&
 						p.addTargetBlankToFullyQualifiedLinks)
 
@@ -418,18 +424,18 @@ func (p *Policy) sanitizeAttrs(
 					for _, htmlAttr := range cleanAttrs {
 
 						var appended bool
-						if htmlAttr.Key == "rel" && addNoFollow {
+						if htmlAttr.Key == "rel" && (addNoFollow || addNoReferrer) {
 
-							if strings.Contains(htmlAttr.Val, "nofollow") {
-								noFollowFound = true
-								tmpAttrs = append(tmpAttrs, htmlAttr)
-								appended = true
-							} else {
+							if addNoFollow && !strings.Contains(htmlAttr.Val, "nofollow") {
 								htmlAttr.Val += " nofollow"
-								noFollowFound = true
-								tmpAttrs = append(tmpAttrs, htmlAttr)
-								appended = true
 							}
+							if addNoReferrer && !strings.Contains(htmlAttr.Val, "noreferrer") {
+								htmlAttr.Val += " noreferrer"
+							}
+							noFollowFound = addNoFollow
+							noReferrerFound = addNoReferrer
+							tmpAttrs = append(tmpAttrs, htmlAttr)
+							appended = true
 						}
 
 						if elementName == "a" && htmlAttr.Key == "target" {
@@ -448,14 +454,22 @@ func (p *Policy) sanitizeAttrs(
 							tmpAttrs = append(tmpAttrs, htmlAttr)
 						}
 					}
-					if noFollowFound || targetBlankFound {
+					if noFollowFound || noReferrerFound || targetBlankFound {
 						cleanAttrs = tmpAttrs
 					}
 
-					if addNoFollow && !noFollowFound {
+					if (addNoFollow && !noFollowFound) || (addNoReferrer && !noReferrerFound) {
 						rel := html.Attribute{}
 						rel.Key = "rel"
-						rel.Val = "nofollow"
+						if addNoFollow {
+							rel.Val = "nofollow"
+						}
+						if addNoReferrer {
+							if rel.Val != "" {
+								rel.Val += " "
+							}
+							rel.Val += "noreferrer"
+						}
 						cleanAttrs = append(cleanAttrs, rel)
 					}
 


Test patch:
diff --git a/sanitize_test.go b/sanitize_test.go
index 373fe0e..8bcddd4 100644
--- a/sanitize_test.go
+++ b/sanitize_test.go
@@ -1586,3 +1586,75 @@ func TestIssue55ScriptTags(t *testing.T) {
 		)
 	}
 }
+
+func TestIssue85NoReferrer(t *testing.T) {
+	p := UGCPolicy()
+	p.AllowAttrs("rel").OnElements("a")
+	p.RequireNoReferrerOnLinks(true)
+	p.AddTargetBlankToFullyQualifiedLinks(true)
+	p.AllowAttrs("target").Matching(Paragraph).OnElements("a")
+
+	tests := []test{
+		{
+			in:       `<a href="/path" />`,
+			expected: `<a href="/path" rel="nofollow noreferrer"/>`,
+		},
+		{
+			in:       `<a href="/path" target="_blank" />`,
+			expected: `<a href="/path" target="_blank" rel="nofollow noreferrer noopener"/>`,
+		},
+		{
+			in:       `<a href="/path" target="foo" />`,
+			expected: `<a href="/path" target="foo" rel="nofollow noreferrer"/>`,
+		},
+		{
+			in:       `<a href="https://www.google.com/" />`,
+			expected: `<a href="https://www.google.com/" rel="nofollow noreferrer noopener" target="_blank"/>`,
+		},
+		{
+			in:       `<a href="https://www.google.com/" target="_blank"/>`,
+			expected: `<a href="https://www.google.com/" target="_blank" rel="nofollow noreferrer noopener"/>`,
+		},
+		{
+			in:       `<a href="https://www.google.com/" rel="nofollow"/>`,
+			expected: `<a href="https://www.google.com/" rel="nofollow noreferrer noopener" target="_blank"/>`,
+		},
+		{
+			in:       `<a href="https://www.google.com/" rel="noopener"/>`,
+			expected: `<a href="https://www.google.com/" rel="noopener nofollow noreferrer" target="_blank"/>`,
+		},
+		{
+			in:       `<a href="https://www.google.com/" rel="noopener nofollow" />`,
+			expected: `<a href="https://www.google.com/" rel="noopener nofollow noreferrer" target="_blank"/>`,
+		},
+		{
+			in:       `<a href="https://www.google.com/" target="foo" />`,
+			expected: `<a href="https://www.google.com/" target="_blank" rel="nofollow noreferrer noopener"/>`,
+		},
+		{
+			in:       `<a href="https://www.google.com/" rel="external"/>`,
+			expected: `<a href="https://www.google.com/" rel="external nofollow noreferrer noopener" target="_blank"/>`,
+		},
+	}
+
+	// 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()
+}