shithub: hugo

Download patch

ref: 724cc0ddff3427a37b1fa4367880fce23bb4f1f8
parent: f5946ea3ddf4ae4256b0ef6a8ccf73fb9d1253cf
author: Anthony Fok <[email protected]>
date: Mon Jan 19 18:41:22 EST 2015

Add `safeUrl`; disable `safeHtmlAttr`; rename `safeCSS` to `safeCss`

- Add `safeUrl` template function (Fixes #347)
- Add TestSafeUrl() fashioned after @tatsushid great examples
- Disable `safeHtmlAttr` pending further discussions on its other
  use cases because `safeUrl` is a cleaner solution to #347.
  (There are also `safeJs` and `safeJsStr` that we could implement
  if there are legitimate demands for them.)
- Rename `safeCSS` to `safeCss` (to follow the convention of `safeHtml`)
- Add/expand documentation on `safeHtml`, `safeCss` and `safeUrl`

--- a/docs/content/templates/functions.md
+++ b/docs/content/templates/functions.md
@@ -263,10 +263,96 @@
 e.g. `<a href="/tags/{{ . | urlize }}">{{ . }}</a>`
 
 ### safeHtml
-Declares the provided string as "safe" so Go templates will not filter it.
+Declares the provided string as a "safe" HTML document fragment
+so Go html/template will not filter it.  It should not be used
+for HTML from a third-party, or HTML with unclosed tags or comments.
 
-e.g. `{{ .Params.CopyrightHTML | safeHtml }}`
+Example: Given a site-wide `config.toml` that contains this line:
 
+    copyright = "© 2015 Jane Doe.  <a href=\"http://creativecommons.org/licenses/by/4.0/\">Some rights reserved</a>."
+
+`{{ .Site.Copyright | safeHtml }}` would then output:
+
+> © 2015 Jane Doe.  <a href="http://creativecommons.org/licenses/by/4.0/">Some rights reserved</a>.
+
+However, without the `safeHtml` function, html/template assumes
+`.Site.Copyright` to be unsafe, escaping all HTML tags,
+rendering the whole string as plain-text like this:
+
+<blockquote>
+<p>© 2015 Jane Doe.  &lt;a href=&#34;http://creativecommons.org/licenses/by/4.0/&#34;&gt;Some rights reserved&lt;/a&gt;.</p>
+</blockquote>
+
+<!--
+### safeHtmlAttr
+Declares the provided string as a "safe" HTML attribute
+from a trusted source, for example, ` dir="ltr"`,
+so Go html/template will not filter it.
+
+Example: Given a site-wide `config.toml` that contains this menu entry:
+
+    [[menu.main]]
+        name = "IRC: #golang at freenode"
+        url = "irc://irc.freenode.net/#golang"
+
+* `<a href="{{ .Url }}">` ⇒ `<a href="#ZgotmplZ">` (Bad!)
+* `<a {{ printf "href=%q" .Url | safeHtmlAttr }}>` ⇒ `<a href="irc://irc.freenode.net/#golang">` (Good!)
+-->
+
+### safeCss
+Declares the provided string as a known "safe" CSS string
+so Go html/templates will not filter it.
+"Safe" means CSS content that matches any of:
+
+1. The CSS3 stylesheet production, such as `p { color: purple }`.
+2. The CSS3 rule production, such as `a[href=~"https:"].foo#bar`.
+3. CSS3 declaration productions, such as `color: red; margin: 2px`.
+4. The CSS3 value production, such as `rgba(0, 0, 255, 127)`.
+
+Example: Given `style = "color: red;"` defined in the front matter of your `.md` file:
+
+* `<p style="{{ .Params.style | safeCss }}">…</p>` ⇒ `<p style="color: red;">…</p>` (Good!)
+* `<p style="{{ .Params.style }}">…</p>` ⇒ `<p style="ZgotmplZ">…</p>` (Bad!)
+
+Note: "ZgotmplZ" is a special value that indicates that unsafe content reached a
+CSS or URL context.
+
+### safeUrl
+Declares the provided string as a "safe" URL or URL substring (see [RFC 3986][]).
+A URL like `javascript:checkThatFormNotEditedBeforeLeavingPage()` from a trusted
+source should go in the page, but by default dynamic `javascript:` URLs are
+filtered out since they are a frequently exploited injection vector.
+
+[RFC 3986]: http://tools.ietf.org/html/rfc3986
+
+Without `safeUrl`, only the URI schemes `http:`, `https:` and `mailto:`
+are considered safe.  All other URI schemes, e.g.&nbsp;`irc:` and
+`javascript:`, get filtered and replaced with the `ZgotmplZ` unsafe
+content indicator.
+
+Example: Given a site-wide `config.toml` that contains this menu entry:
+
+    [[menu.main]]
+        name = "IRC: #golang at freenode"
+        url = "irc://irc.freenode.net/#golang"
+
+The following template:
+
+    <ul class="sidebar-menu">
+      {{ range .Site.Menus.main }}
+      <li><a href="{{ .Url }}">{{ .Name }}</a></li>
+      {{ end }}
+    </ul>
+
+would produce `<li><a href="#ZgotmplZ">IRC: #golang at freenode</a></li>`
+for the `irc://…` URL.
+
+To fix this, add ` | safeUrl` after `.Url` on the 3rd line, like this:
+
+      <li><a href="{{ .Url | safeUrl }}">{{ .Name }}</a></li>
+
+With this change, we finally get `<li><a href="irc://irc.freenode.net/#golang">IRC: #golang at freenode</a></li>`
+as intended.
 
 ### markdownify
 
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -910,14 +910,20 @@
 	return template.HTML(text)
 }
 
+// "safeHtmlAttr" is currently disabled, pending further discussion
+// on its use case.  2015-01-19
 func SafeHtmlAttr(text string) template.HTMLAttr {
 	return template.HTMLAttr(text)
 }
 
-func SafeCSS(text string) template.CSS {
+func SafeCss(text string) template.CSS {
 	return template.CSS(text)
 }
 
+func SafeUrl(text string) template.URL {
+	return template.URL(text)
+}
+
 func doArithmetic(a, b interface{}, op rune) (interface{}, error) {
 	av := reflect.ValueOf(a)
 	bv := reflect.ValueOf(b)
@@ -1251,8 +1257,8 @@
 		"isset":        IsSet,
 		"echoParam":    ReturnWhenSet,
 		"safeHtml":     SafeHtml,
-		"safeHtmlAttr": SafeHtmlAttr,
-		"safeCSS":      SafeCSS,
+		"safeCss":      SafeCss,
+		"safeUrl":      SafeUrl,
 		"markdownify":  Markdownify,
 		"first":        First,
 		"where":        Where,
--- a/tpl/template_test.go
+++ b/tpl/template_test.go
@@ -898,7 +898,7 @@
 	}
 }
 
-func TestSafeCSS(t *testing.T) {
+func TestSafeCss(t *testing.T) {
 	for i, this := range []struct {
 		str                 string
 		tmplStr             string
@@ -910,6 +910,7 @@
 		tmpl, err := template.New("test").Parse(this.tmplStr)
 		if err != nil {
 			t.Errorf("[%d] unable to create new html template %q: %s", this.tmplStr, err)
+			continue
 		}
 
 		buf := new(bytes.Buffer)
@@ -922,12 +923,47 @@
 		}
 
 		buf.Reset()
-		err = tmpl.Execute(buf, SafeCSS(this.str))
+		err = tmpl.Execute(buf, SafeCss(this.str))
 		if err != nil {
-			t.Errorf("[%d] execute template with an escaped string value by SafeCSS returns unexpected error: %s", i, err)
+			t.Errorf("[%d] execute template with an escaped string value by SafeCss returns unexpected error: %s", i, err)
 		}
 		if buf.String() != this.expectWithEscape {
-			t.Errorf("[%d] execute template with an escaped string value by SafeCSS, got %v but expected %v", i, buf.String(), this.expectWithEscape)
+			t.Errorf("[%d] execute template with an escaped string value by SafeCss, got %v but expected %v", i, buf.String(), this.expectWithEscape)
+		}
+	}
+}
+
+func TestSafeUrl(t *testing.T) {
+	for i, this := range []struct {
+		str                 string
+		tmplStr             string
+		expectWithoutEscape string
+		expectWithEscape    string
+	}{
+		{`irc://irc.freenode.net/#golang`, `<a href="{{ . }}">IRC</a>`, `<a href="#ZgotmplZ">IRC</a>`, `<a href="irc://irc.freenode.net/#golang">IRC</a>`},
+	} {
+		tmpl, err := template.New("test").Parse(this.tmplStr)
+		if err != nil {
+			t.Errorf("[%d] unable to create new html template %q: %s", this.tmplStr, err)
+			continue
+		}
+
+		buf := new(bytes.Buffer)
+		err = tmpl.Execute(buf, this.str)
+		if err != nil {
+			t.Errorf("[%d] execute template with a raw string value returns unexpected error: %s", i, err)
+		}
+		if buf.String() != this.expectWithoutEscape {
+			t.Errorf("[%d] execute template with a raw string value, got %v but expected %v", i, buf.String(), this.expectWithoutEscape)
+		}
+
+		buf.Reset()
+		err = tmpl.Execute(buf, SafeUrl(this.str))
+		if err != nil {
+			t.Errorf("[%d] execute template with an escaped string value by SafeUrl returns unexpected error: %s", i, err)
+		}
+		if buf.String() != this.expectWithEscape {
+			t.Errorf("[%d] execute template with an escaped string value by SafeUrl, got %v but expected %v", i, buf.String(), this.expectWithEscape)
 		}
 	}
 }