From 056b0edcb8c152152021eebf4cf42adbfbe77992 Mon Sep 17 00:00:00 2001
From: Roland Shoemaker <roland@golang.org>
Date: Wed, 14 Feb 2024 17:18:36 -0800
Subject: [PATCH] [release-branch.go1.22] html/template: escape additional
 tokens in MarshalJSON errors

Escape "</script" and "<!--" in errors returned from MarshalJSON errors
when attempting to marshal types in script blocks. This prevents any
user controlled content from prematurely terminating the script block.

Updates #65697
Fixes #65969

Change-Id: Icf0e26c54ea7d9c1deed0bff11b6506c99ddef1b
Reviewed-on: https://go-review.googlesource.com/c/go/+/564196
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
(cherry picked from commit ccbc725f2d678255df1bd326fa511a492aa3a0aa)
Reviewed-on: https://go-review.googlesource.com/c/go/+/567535
Reviewed-by: Carlos Amedee <carlos@golang.org>

Upstream-Status: Backport [https://github.com/golang/go/commit/056b0edcb8c152152021eebf4cf42adbfbe77992]
CVE: CVE-2024-24785
Signed-off-by: Vivek Kumbhar <vkumbhar@mvista.com>
---
 src/html/template/js.go      | 22 ++++++++-
 src/html/template/js_test.go | 96 ++++++++++++++++++++----------------
 2 files changed, 74 insertions(+), 44 deletions(-)

diff --git a/src/html/template/js.go b/src/html/template/js.go
index 35994f0..4d3b25d 100644
--- a/src/html/template/js.go
+++ b/src/html/template/js.go
@@ -171,13 +171,31 @@ func jsValEscaper(args ...interface{}) string {
	// cyclic data. This may be an unacceptable DoS risk.
	b, err := json.Marshal(a)
	if err != nil {
-		// Put a space before comment so that if it is flush against
+		// While the standard JSON marshaller does not include user controlled
+		// information in the error message, if a type has a MarshalJSON method,
+		// the content of the error message is not guaranteed. Since we insert
+		// the error into the template, as part of a comment, we attempt to
+		// prevent the error from either terminating the comment, or the script
+		// block itself.
+		//
+		// In particular we:
+		//   * replace "*/" comment end tokens with "* /", which does not
+		//     terminate the comment
+		//   * replace "</script" with "\x3C/script", and "<!--" with
+		//     "\x3C!--", which prevents confusing script block termination
+		//     semantics
+		//
+		// We also put a space before the comment so that if it is flush against
		// a division operator it is not turned into a line comment:
		//     x/{{y}}
		// turning into
		//     x//* error marshaling y:
		//          second line of error message */null
-		return fmt.Sprintf(" /* %s */null ", strings.ReplaceAll(err.Error(), "*/", "* /"))
+		errStr := err.Error()
+		errStr = strings.ReplaceAll(errStr, "*/", "* /")
+		errStr = strings.ReplaceAll(errStr, "</script", `\x3C/script`)
+		errStr = strings.ReplaceAll(errStr, "<!--", `\x3C!--`)
+		return fmt.Sprintf(" /* %s */null ", errStr)
	}

	// TODO: maybe post-process output to prevent it from containing
diff --git a/src/html/template/js_test.go b/src/html/template/js_test.go
index de9ef28..0eaec11 100644
--- a/src/html/template/js_test.go
+++ b/src/html/template/js_test.go
@@ -5,6 +5,7 @@
 package template

 import (
+        "errors"
	"bytes"
	"math"
	"strings"
@@ -104,61 +105,72 @@ func TestNextJsCtx(t *testing.T) {
	}
 }

+type jsonErrType struct{}
+
+func (e *jsonErrType) MarshalJSON() ([]byte, error) {
+	return nil, errors.New("beep */ boop </script blip <!--")
+}
+
 func TestJSValEscaper(t *testing.T) {
	tests := []struct {
-		x  interface{}
-		js string
+		x        any
+		js       string
+		skipNest bool
	}{
-		{int(42), " 42 "},
-		{uint(42), " 42 "},
-		{int16(42), " 42 "},
-		{uint16(42), " 42 "},
-		{int32(-42), " -42 "},
-		{uint32(42), " 42 "},
-		{int16(-42), " -42 "},
-		{uint16(42), " 42 "},
-		{int64(-42), " -42 "},
-		{uint64(42), " 42 "},
-		{uint64(1) << 53, " 9007199254740992 "},
+		{int(42), " 42 ", false},
+		{uint(42), " 42 ", false},
+		{int16(42), " 42 ", false},
+		{uint16(42), " 42 ", false},
+		{int32(-42), " -42 ", false},
+		{uint32(42), " 42 ", false},
+		{int16(-42), " -42 ", false},
+		{uint16(42), " 42 ", false},
+		{int64(-42), " -42 ", false},
+		{uint64(42), " 42 ", false},
+		{uint64(1) << 53, " 9007199254740992 ", false},
		// ulp(1 << 53) > 1 so this loses precision in JS
		// but it is still a representable integer literal.
-		{uint64(1)<<53 + 1, " 9007199254740993 "},
-		{float32(1.0), " 1 "},
-		{float32(-1.0), " -1 "},
-		{float32(0.5), " 0.5 "},
-		{float32(-0.5), " -0.5 "},
-		{float32(1.0) / float32(256), " 0.00390625 "},
-		{float32(0), " 0 "},
-		{math.Copysign(0, -1), " -0 "},
-		{float64(1.0), " 1 "},
-		{float64(-1.0), " -1 "},
-		{float64(0.5), " 0.5 "},
-		{float64(-0.5), " -0.5 "},
-		{float64(0), " 0 "},
-		{math.Copysign(0, -1), " -0 "},
-		{"", `""`},
-		{"foo", `"foo"`},
+		{uint64(1)<<53 + 1, " 9007199254740993 ", false},
+		{float32(1.0), " 1 ", false},
+		{float32(-1.0), " -1 ", false},
+		{float32(0.5), " 0.5 ", false},
+		{float32(-0.5), " -0.5 ", false},
+		{float32(1.0) / float32(256), " 0.00390625 ", false},
+		{float32(0), " 0 ", false},
+		{math.Copysign(0, -1), " -0 ", false},
+		{float64(1.0), " 1 ", false},
+		{float64(-1.0), " -1 ", false},
+		{float64(0.5), " 0.5 ", false},
+		{float64(-0.5), " -0.5 ", false},
+		{float64(0), " 0 ", false},
+		{math.Copysign(0, -1), " -0 ", false},
+		{"", `""`, false},
+		{"foo", `"foo"`, false},
		// Newlines.
-		{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`},
+		{"\r\n\u2028\u2029", `"\r\n\u2028\u2029"`, false},
		// "\v" == "v" on IE 6 so use "\u000b" instead.
-		{"\t\x0b", `"\t\u000b"`},
-		{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`},
-		{[]interface{}{}, "[]"},
-		{[]interface{}{42, "foo", nil}, `[42,"foo",null]`},
-		{[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`},
-		{"<!--", `"\u003c!--"`},
-		{"-->", `"--\u003e"`},
-		{"<![CDATA[", `"\u003c![CDATA["`},
-		{"]]>", `"]]\u003e"`},
-		{"</script", `"\u003c/script"`},
-		{"\U0001D11E", "\"\U0001D11E\""}, // or "\uD834\uDD1E"
-		{nil, " null "},
+		{"\t\x0b", `"\t\u000b"`, false},
+		{struct{ X, Y int }{1, 2}, `{"X":1,"Y":2}`, false},
+		{[]any{}, "[]", false},
+		{[]any{42, "foo", nil}, `[42,"foo",null]`, false},
+		{[]string{"<!--", "</script>", "-->"}, `["\u003c!--","\u003c/script\u003e","--\u003e"]`, false},
+		{"<!--", `"\u003c!--"`, false},
+		{"-->", `"--\u003e"`, false},
+		{"<![CDATA[", `"\u003c![CDATA["`, false},
+		{"]]>", `"]]\u003e"`, false},
+		{"</script", `"\u003c/script"`, false},
+		{"\U0001D11E", "\"\U0001D11E\"", false}, // or "\uD834\uDD1E"
+		{nil, " null ", false},
+		{&jsonErrType{}, " /* json: error calling MarshalJSON for type *template.jsonErrType: beep * / boop \\x3C/script blip \\x3C!-- */null ", true},
	}

	for _, test := range tests {
		if js := jsValEscaper(test.x); js != test.js {
			t.Errorf("%+v: want\n\t%q\ngot\n\t%q", test.x, test.js, js)
		}
+		if test.skipNest {
+			continue
+		}
		// Make sure that escaping corner cases are not broken
		// by nesting.
		a := []interface{}{test.x}
--
2.39.3
