From 94e9a5e19b831504eca2b7202b78d1a48c4be547 Mon Sep 17 00:00:00 2001 From: Roberto Clapis Date: Mon, 18 Nov 2019 10:05:07 +0100 Subject: [PATCH] text/template: harden JSEscape to also escape ampersand and equal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ampersand and equal are not dangerous in a JS/JSString context but they might cause issues if interpolated in HTML attributes. This change makes it harder to introduce XSS by misusing escaping. Thanks to t1ddl3r for reporting this common misuse scenario. Fixes #35665 Change-Id: Ice6416477bba4cb2ba2fe2cfdc20e027957255c0 Reviewed-on: https://go-review.googlesource.com/c/go/+/207637 Reviewed-by: Filippo Valsorda Reviewed-by: Mike Samuel Reviewed-by: Andrew Bonventre Reviewed-by: Daniel Martí --- src/html/template/example_test.go | 6 +++--- src/text/template/exec_test.go | 2 ++ src/text/template/funcs.go | 8 +++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/html/template/example_test.go b/src/html/template/example_test.go index 533c0dd9616..9d965f1943c 100644 --- a/src/html/template/example_test.go +++ b/src/html/template/example_test.go @@ -116,9 +116,9 @@ func Example_escape() { // "Fran & Freddie's Diner" <tasty@example.com> // "Fran & Freddie's Diner" <tasty@example.com> // "Fran & Freddie's Diner"32<tasty@example.com> - // \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E - // \"Fran & Freddie\'s Diner\" \x3Ctasty@example.com\x3E - // \"Fran & Freddie\'s Diner\"32\x3Ctasty@example.com\x3E + // \"Fran \x26 Freddie\'s Diner\" \x3Ctasty@example.com\x3E + // \"Fran \x26 Freddie\'s Diner\" \x3Ctasty@example.com\x3E + // \"Fran \x26 Freddie\'s Diner\"32\x3Ctasty@example.com\x3E // %22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E } diff --git a/src/text/template/exec_test.go b/src/text/template/exec_test.go index 2b299b0bf61..aa5cd4c5525 100644 --- a/src/text/template/exec_test.go +++ b/src/text/template/exec_test.go @@ -909,6 +909,8 @@ func TestJSEscaping(t *testing.T) { {`Yukihiro says "今日は世界"`, `Yukihiro says \"今日は世界\"`}, {"unprintable \uFDFF", `unprintable \uFDFF`}, {``, `\x3Chtml\x3E`}, + {`no = in attributes`, `no \x3D in attributes`}, + {`' does not become HTML entity`, `\x26#x27; does not become HTML entity`}, } for _, tc := range testCases { s := JSEscapeString(tc.in) diff --git a/src/text/template/funcs.go b/src/text/template/funcs.go index 0985eda3175..0568c798a84 100644 --- a/src/text/template/funcs.go +++ b/src/text/template/funcs.go @@ -642,6 +642,8 @@ var ( jsQuot = []byte(`\"`) jsLt = []byte(`\x3C`) jsGt = []byte(`\x3E`) + jsAmp = []byte(`\x26`) + jsEq = []byte(`\x3D`) ) // JSEscape writes to w the escaped JavaScript equivalent of the plain text data b. @@ -670,6 +672,10 @@ func JSEscape(w io.Writer, b []byte) { w.Write(jsLt) case '>': w.Write(jsGt) + case '&': + w.Write(jsAmp) + case '=': + w.Write(jsEq) default: w.Write(jsLowUni) t, b := c>>4, c&0x0f @@ -704,7 +710,7 @@ func JSEscapeString(s string) string { func jsIsSpecial(r rune) bool { switch r { - case '\\', '\'', '"', '<', '>': + case '\\', '\'', '"', '<', '>', '&', '=': return true } return r < ' ' || utf8.RuneSelf <= r