├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── conversions_test.go ├── doc.go ├── example_test.go ├── go.mod ├── go.sum ├── html.go ├── html_test.go ├── identifier.go ├── identifier_test.go ├── init.go ├── internal ├── raw │ └── raw.go ├── safehtmlutil │ ├── safehtmlutil.go │ └── safehtmlutil_test.go └── template │ └── raw │ └── raw.go ├── legacyconversions └── legacyconversions.go ├── script.go ├── script_test.go ├── style.go ├── style_test.go ├── stylesheet.go ├── stylesheet_test.go ├── template ├── clone_test.go ├── content_test.go ├── context.go ├── delim_string.go ├── doc.go ├── error.go ├── escape.go ├── escape_test.go ├── example_test.go ├── examplefiles_test.go ├── init.go ├── redefine_test.go ├── sanitize.go ├── sanitize_test.go ├── sanitizers.go ├── script_example_test.go ├── state_string.go ├── template.go ├── template_test.go ├── testdata │ ├── dir1 │ │ └── parsefiles_t1.tmpl │ ├── dir2 │ │ └── parsefiles_t2.tmpl │ ├── glob_t0.tmpl │ ├── glob_t1.tmpl │ ├── glob_t2.tmpl │ ├── helpers_t1.tmpl │ ├── helpers_t2.tmpl │ ├── share_t0.tmpl │ └── share_t1.tmpl ├── transition.go ├── transition_test.go ├── trustedfs.go ├── trustedfs_test.go ├── trustedsource.go ├── trustedsource_test.go ├── trustedtemplate.go ├── trustedtemplate_test.go ├── uncheckedconversions │ ├── uncheckedconversions.go │ └── uncheckedconversions_test.go ├── url.go └── url_test.go ├── testconversions └── testconversions.go ├── trustedresourceurl.go ├── trustedresourceurl_test.go ├── uncheckedconversions └── uncheckedconversions.go ├── url.go └── urlset.go /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Community Guidelines 27 | 28 | This project follows 29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Safe HTML for Go 2 | 3 | `safehtml` provides immutable string-like types that wrap web types such as 4 | HTML, JavaScript and CSS. These wrappers are safe by construction against XSS 5 | and similar web vulnerabilities, and they can only be interpolated in safe ways. 6 | You can read more about our approach to web security in our 7 | [whitepaper](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/42934.pdf), 8 | or this [OWASP talk](https://www.youtube.com/watch?v=ccfEu-Jj0as). 9 | 10 | Additional subpackages provide APIs for managing exceptions to the 11 | safety rules, and a template engine with a syntax and interface that closely 12 | matches [`html/template`](https://golang.org/pkg/html/template/). You can refer 13 | to the [godoc](https://pkg.go.dev/github.com/google/safehtml?tab=doc) 14 | for each (sub)package for the API documentation and code examples. 15 | More end-to-end demos are available in `example_test.go`. 16 | 17 | This is not an officially supported Google product. 18 | -------------------------------------------------------------------------------- /conversions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml_test 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/google/safehtml/legacyconversions" 13 | "github.com/google/safehtml/testconversions" 14 | "github.com/google/safehtml/uncheckedconversions" 15 | ) 16 | 17 | const html = `this is not a valid safehtml.Script` 34 | 35 | func TestScriptFromStringKnownToSatisfyTypeContract(t *testing.T) { 36 | if out := uncheckedconversions.ScriptFromStringKnownToSatisfyTypeContract(script).String(); script != out { 37 | t.Errorf("uncheckedconversions.ScriptFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q", 38 | script, out, script) 39 | } 40 | if out := legacyconversions.RiskilyAssumeScript(script).String(); script != out { 41 | t.Errorf("legacyconversions.RiskilyAssumeScript(%q).String() = %q, want %q", 42 | script, out, script) 43 | } 44 | if out := testconversions.MakeScriptForTest(script).String(); script != out { 45 | t.Errorf("testconversions.MakeScriptForTest(%q).String() = %q, want %q", 46 | script, out, script) 47 | } 48 | } 49 | 50 | const style = `width:expression(this is not valid safehtml.Style` 51 | 52 | func TestStyleFromStringKnownToSatisfyTypeContract(t *testing.T) { 53 | if out := uncheckedconversions.StyleFromStringKnownToSatisfyTypeContract(style).String(); style != out { 54 | t.Errorf("uncheckedconversions.StyleFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q", 55 | style, out, style) 56 | } 57 | if out := legacyconversions.RiskilyAssumeStyle(style).String(); style != out { 58 | t.Errorf("legacyconversions.RiskilyAssumeStyle(%q).String() = %q, want %q", 59 | style, out, style) 60 | } 61 | if out := testconversions.MakeStyleForTest(style).String(); style != out { 62 | t.Errorf("testconversions.MakeStyleForTest(%q).String() = %q, want %q", 63 | style, out, style) 64 | } 65 | } 66 | 67 | const styleSheet = `P { text: }` 68 | 69 | func TestStyleSheetFromStringKnownToSatisfyTypeContract(t *testing.T) { 70 | if out := uncheckedconversions.StyleSheetFromStringKnownToSatisfyTypeContract(styleSheet).String(); styleSheet != out { 71 | t.Errorf("uncheckedconversions.StyleSheetFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q", 72 | styleSheet, out, styleSheet) 73 | } 74 | if out := legacyconversions.RiskilyAssumeStyleSheet(styleSheet).String(); styleSheet != out { 75 | t.Errorf("legacyconversions.RiskilyAssumeStyleSheet(%q).String() = %q, want %q", 76 | styleSheet, out, styleSheet) 77 | } 78 | if out := testconversions.MakeStyleSheetForTest(styleSheet).String(); styleSheet != out { 79 | t.Errorf("testconversions.MakeStyleSheetForTest(%q).String() = %q, want %q", 80 | styleSheet, out, styleSheet) 81 | } 82 | } 83 | 84 | const url = `data:this will not be sanitized` 85 | 86 | func TestURLFromStringKnownToSatisfyTypeContract(t *testing.T) { 87 | if out := uncheckedconversions.URLFromStringKnownToSatisfyTypeContract(url).String(); url != out { 88 | t.Errorf("uncheckedconversions.URLFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q", 89 | url, out, url) 90 | } 91 | if out := legacyconversions.RiskilyAssumeURL(url).String(); url != out { 92 | t.Errorf("legacyconversions.RiskilyAssumeURL(%q).String() = %q, want %q", 93 | url, out, url) 94 | } 95 | if out := testconversions.MakeURLForTest(url).String(); url != out { 96 | t.Errorf("testconversions.MakeURLForTest(%q).String() = %q, want %q", 97 | url, out, url) 98 | } 99 | } 100 | 101 | const tru = `data:this will not be sanitized` 102 | 103 | func TestTrustedResourceURLFromStringKnownToSatisfyTypeContract(t *testing.T) { 104 | if out := uncheckedconversions.TrustedResourceURLFromStringKnownToSatisfyTypeContract(tru).String(); tru != out { 105 | t.Errorf("uncheckedconversions.TrustedResourceURLFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q", 106 | tru, out, tru) 107 | } 108 | if out := legacyconversions.RiskilyAssumeTrustedResourceURL(tru).String(); tru != out { 109 | t.Errorf("legacyconversions.RiskilyAssumeTrustedResourceURL(%q).String() = %q, want %q", 110 | tru, out, tru) 111 | } 112 | if out := testconversions.MakeTrustedResourceURLForTest(tru).String(); tru != out { 113 | t.Errorf("testconversions.MakeTrustedResourceURLForTest(%q).String() = %q, want %q", 114 | tru, out, tru) 115 | } 116 | } 117 | 118 | const identifier = `1nvalid-identifier-starting-with-a-digit` 119 | 120 | func TestIdentifierFromStringKnownToSatisfyTypeContract(t *testing.T) { 121 | if out := uncheckedconversions.IdentifierFromStringKnownToSatisfyTypeContract(identifier).String(); identifier != out { 122 | t.Errorf("uncheckedconversions.IdentifierFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q", 123 | identifier, out, identifier) 124 | } 125 | if out := legacyconversions.RiskilyAssumeIdentifier(identifier).String(); identifier != out { 126 | t.Errorf("legacyconversions.RiskilyAssumeIdentifier(%q).String() = %q, want %q", 127 | identifier, out, identifier) 128 | } 129 | if out := testconversions.MakeIdentifierForTest(identifier).String(); identifier != out { 130 | t.Errorf("testconversions.MakeIdentifierForTest(%q).String() = %q, want %q", 131 | identifier, out, identifier) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // Package safehtml provides immutable string-like types which represent values that 8 | // are guaranteed to be safe, by construction or by escaping or sanitization, to use 9 | // in various HTML contexts and with various DOM APIs. 10 | package safehtml 11 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml_test 8 | 9 | import ( 10 | "bytes" 11 | "testing" 12 | 13 | "github.com/google/safehtml" 14 | "github.com/google/safehtml/template" 15 | ) 16 | 17 | func TestExampleScriptFromDataAndConstant(t *testing.T) { 18 | const script = `alert(msg['Greeting'] + ' ' + msg['Names'] + '! The year is ' + msg['Year'])` 19 | type WelcomeMessage struct { 20 | Greeting string 21 | Names []string 22 | Year int 23 | } 24 | data := WelcomeMessage{ 25 | Greeting: "Hello", 26 | Names: []string{"Alice", "Bob"}, 27 | Year: 3055, 28 | } 29 | s, err := safehtml.ScriptFromDataAndConstant("msg", data, script) 30 | if err != nil { 31 | t.Fatalf("while building script from data: %v", err) 32 | } 33 | t.Log(s) 34 | // Output: 35 | // var msg = {"Greeting":"Hello","Names":["Alice","Bob"],"Year":3055}; 36 | // alert(msg['Greeting'] + ' ' + msg['Names'] + '! The year is ' + msg['Year']) 37 | } 38 | 39 | // ScriptFromDataAndConstant can be used to pass dynamic data from 40 | // a Go program to inline scripts in a safehtml/template Template. 41 | func TestExampleScriptFromDataAndConstant_safeHTMLTemplate(t *testing.T) { 42 | tmpl := template.Must(template.New("").Parse(` 43 | 44 | 45 | {{ if .IsIntro }} 46 |

Welcome!

47 | {{ else }} 48 |

Nice to see you again!

49 | {{ end }} 50 | 55 | 56 | `)) 57 | data := struct { 58 | Name string 59 | ID int 60 | }{ 61 | getName(), 62 | getID(), 63 | } 64 | script, err := safehtml.ScriptFromDataAndConstant( 65 | "myArgs", data, ` my.functionCall(myArgs[‘Name’], myArgs[‘ID’])`) 66 | if err != nil { 67 | t.Fatalf("while building script from data: %v", err) 68 | } 69 | out := &bytes.Buffer{} 70 | if err := tmpl.Execute(out, struct { 71 | IsIntro bool 72 | GoodPoints []string 73 | Script safehtml.Script 74 | }{false, []string{"foo", "bar"}, script}); err != nil { 75 | t.Fatalf("while rendering template: %v", err) 76 | } 77 | t.Log(out) 78 | // Output: 79 | // 80 | // 81 | // ... 82 | // 86 | // 87 | } 88 | 89 | func getName() string { 90 | return "Sam" 91 | } 92 | 93 | func getID() int { 94 | return 14 95 | } 96 | 97 | func TestExampleTrustedResourceURLFormat(t *testing.T) { 98 | tru, err := safehtml.TrustedResourceURLFormatFromConstant(`//www.youtube.com/v/%{id}?hl=%{lang}`, map[string]string{ 99 | "id": "abc0def1", 100 | "lang": "en", 101 | }) 102 | if err != nil { 103 | t.Fatalf("while building URL: %v", err) 104 | } 105 | t.Log(tru) 106 | // Output: 107 | // //www.youtube.com/v/abc0def1?hl=en 108 | } 109 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/safehtml 2 | 3 | go 1.16 4 | 5 | require golang.org/x/text v0.3.3 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 2 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 3 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= 4 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 5 | -------------------------------------------------------------------------------- /html.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "bytes" 11 | "html" 12 | "unicode" 13 | 14 | "golang.org/x/text/unicode/rangetable" 15 | ) 16 | 17 | // An HTML is an immutable string-like type that is safe to use in HTML 18 | // contexts in DOM APIs and HTML documents. 19 | // 20 | // HTML guarantees that its value as a string will not cause untrusted script 21 | // execution when evaluated as HTML in a browser. 22 | // 23 | // Values of this type are guaranteed to be safe to use in HTML contexts, 24 | // such as assignment to the innerHTML DOM property, or interpolation into an 25 | // HTML template in HTML PC_DATA context, in the sense that the use will not 26 | // result in a Cross-site Scripting (XSS) vulnerability. 27 | type HTML struct { 28 | // We declare an HTML not as a string but as a struct wrapping a string 29 | // to prevent construction of HTML values through string conversion. 30 | str string 31 | } 32 | 33 | // HTMLer is implemented by any value that has an HTML method, which defines the 34 | // safe HTML format for that value. 35 | type HTMLer interface { 36 | HTML() HTML 37 | } 38 | 39 | // HTMLEscaped returns an HTML whose value is text, with the characters [&<>"'] escaped. 40 | // 41 | // text is coerced to interchange valid, so the resulting HTML contains only 42 | // valid UTF-8 characters which are legal in HTML and XML. 43 | func HTMLEscaped(text string) HTML { 44 | return HTML{escapeAndCoerceToInterchangeValid(text)} 45 | } 46 | 47 | // HTMLConcat returns an HTML which contains, in order, the string representations 48 | // of the given htmls. 49 | func HTMLConcat(htmls ...HTML) HTML { 50 | var b bytes.Buffer 51 | for _, html := range htmls { 52 | b.WriteString(html.String()) 53 | } 54 | return HTML{b.String()} 55 | } 56 | 57 | // String returns the string form of the HTML. 58 | func (h HTML) String() string { 59 | return h.str 60 | } 61 | 62 | // escapeAndCoerceToInterchangeValid coerces the string to interchange-valid 63 | // UTF-8 and then HTML-escapes it. 64 | func escapeAndCoerceToInterchangeValid(str string) string { 65 | return html.EscapeString(coerceToUTF8InterchangeValid(str)) 66 | } 67 | 68 | // coerceToUTF8InterchangeValid coerces a string to interchange-valid UTF-8. 69 | // Illegal UTF-8 bytes are replaced with the Unicode replacement character 70 | // ('\uFFFD'). C0 and C1 control codes (other than CR LF HT FF) and 71 | // non-characters are also replaced with the Unicode replacement character. 72 | func coerceToUTF8InterchangeValid(s string) string { 73 | // TODO: Replace this entire function with stdlib function if https://golang.org/issue/25805 gets addressed. 74 | runes := make([]rune, 0, len(s)) 75 | // If s contains any invalid UTF-8 byte sequences, range will have rune 76 | // contain the Unicode replacement character and there's no need to call 77 | // utf8.ValidRune. I.e. iteration over the string implements 78 | // CoerceToStructurallyValid() from C++/Java. 79 | // See https://blog.golang.org/strings. 80 | for _, rune := range s { 81 | if unicode.Is(controlAndNonCharacter, rune) { 82 | runes = append(runes, unicode.ReplacementChar) 83 | } else { 84 | runes = append(runes, rune) 85 | } 86 | } 87 | return string(runes) 88 | } 89 | 90 | // controlAndNonCharacters contains the non-interchange-valid codepoints. 91 | // 92 | // See http://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream 93 | // 94 | // safehtml functions do a lot of lookups on these tables, so merging them is probably 95 | // worth it to avoid comparing against both tables each time. 96 | var controlAndNonCharacter = rangetable.Merge(unicode.Noncharacter_Code_Point, controlChar) 97 | 98 | // controlChar contains Unicode control characters disallowed in interchange 99 | // valid UTF-8. This table is slightly different from unicode.Cc: 100 | // - Disallows null. 101 | // - Allows LF, CR, HT, and FF. 102 | // 103 | // unicode.C is mentioned in unicode.IsControl; it contains "special" characters 104 | // which includes at least control characters, surrogate code points, and 105 | // formatting codepoints (e.g. word joiner). We don't need to exclude all of 106 | // those. In particular, surrogates are handled by the for loop converting 107 | // invalid UTF-8 byte sequences to the Unicode replacement character. 108 | var controlChar = &unicode.RangeTable{ 109 | R16: []unicode.Range16{ 110 | {0x0000, 0x0008, 1}, 111 | {0x000B, 0x000B, 1}, 112 | {0x000E, 0x001F, 1}, 113 | {0x007F, 0x009F, 1}, 114 | }, 115 | LatinOffset: 4, 116 | } 117 | -------------------------------------------------------------------------------- /html_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | const ( 14 | rawHTML = `<>'"&` 15 | escapedHTML = "<>'"&" 16 | ) 17 | 18 | func TestHTMLEscaped(t *testing.T) { 19 | if got := HTMLEscaped(rawHTML); got.String() != escapedHTML { 20 | t.Errorf("HTMLEscaped(%q) == %q, want %q", rawHTML, got.String(), escapedHTML) 21 | } 22 | } 23 | 24 | func TestHTMLConcat(t *testing.T) { 25 | for _, test := range [...]struct { 26 | in []string 27 | want string 28 | }{ 29 | { 30 | nil, 31 | "", 32 | }, 33 | { 34 | []string{""}, 35 | "", 36 | }, 37 | { 38 | []string{"Hello world!"}, 39 | "Hello world!", 40 | }, 41 | { 42 | []string{"Hello", " ", "world!"}, 43 | "Hello world!", 44 | }, 45 | } { 46 | var htmls []HTML 47 | for _, str := range test.in { 48 | htmls = append(htmls, HTML{str}) 49 | } 50 | if got := HTMLConcat(htmls...); got.String() != test.want { 51 | t.Errorf("HTMLConcat with args %q returns %q, want %q", test.in, got.String(), test.want) 52 | } 53 | } 54 | } 55 | 56 | func TestCoerceToInterchangeValid(t *testing.T) { 57 | // Single character tests 58 | for _, tt := range [...]struct { 59 | in string 60 | replaced bool 61 | }{ 62 | // Control characters. 63 | {"\x00", true}, 64 | {"\x04", true}, 65 | {"\x08", true}, 66 | {"\t", false}, // x09 67 | {"\n", false}, // x0A 68 | {"\v", true}, // x0B 69 | {"\f", false}, // x0C 70 | {"\r", false}, // x0D 71 | {"\x0E", true}, 72 | {"\x0F", true}, 73 | // Non-character codepoints. See 74 | // http://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream. 75 | {"\uFDCF", false}, // Begin border of \uFDD0 to \uFDEF range. 76 | {"\uFDD0", true}, // Begin range. 77 | {"\uFDD0", true}, // Mid range. 78 | {"\uFDEF", true}, // End range. 79 | {"\uFDF0", false}, // End border. 80 | {"\uFFFE", true}, 81 | {"\uFFFF", true}, 82 | {"\U0001FFFE", true}, 83 | {"\U0001FFFF", true}, 84 | {"\U0002FFFE", true}, 85 | {"\U0002FFFF", true}, 86 | {"\U0003FFFE", true}, 87 | {"\U0003FFFF", true}, 88 | {"\U0004FFFE", true}, 89 | {"\U0004FFFF", true}, 90 | {"\U0005FFFE", true}, 91 | {"\U0005FFFF", true}, 92 | {"\U0006FFFE", true}, 93 | {"\U0006FFFF", true}, 94 | {"\U0007FFFE", true}, 95 | {"\U0007FFFF", true}, 96 | {"\U0008FFFE", true}, 97 | {"\U0008FFFF", true}, 98 | {"\U0009FFFE", true}, 99 | {"\U0009FFFF", true}, 100 | {"\U000AFFFE", true}, 101 | {"\U000AFFFF", true}, 102 | {"\U000BFFFE", true}, 103 | {"\U000BFFFF", true}, 104 | {"\U000CFFFE", true}, 105 | {"\U000CFFFF", true}, 106 | {"\U000DFFFE", true}, 107 | {"\U000DFFFF", true}, 108 | {"\U000EFFFE", true}, 109 | {"\U000EFFFF", true}, 110 | {"\U000FFFFE", true}, 111 | {"\U000FFFFF", true}, 112 | {"\U0010FFFE", true}, 113 | {"\U0010FFFF", true}, 114 | // Invalid UTF8. 115 | {"\xed", true}, 116 | // Valid UTF8. 117 | {" ", false}, 118 | // Replacement character. 119 | {"\uFFFD", false}, 120 | } { 121 | coerced := coerceToUTF8InterchangeValid(tt.in) 122 | if tt.replaced && coerced != "\uFFFD" { 123 | t.Errorf(`coerceToInterchangeValid(%q) == %q, want "0xFFFD"`, tt.in, coerced) 124 | } else if !tt.replaced && coerced != tt.in { 125 | t.Errorf("coerceToInterchangeValid(%q) == %q, want %q", tt.in, coerced, tt.in) 126 | } 127 | } 128 | 129 | // String tests 130 | for _, tt := range [...]struct { 131 | in string 132 | expected string 133 | }{ 134 | {"abcd", "abcd"}, 135 | {"丄ê𐒖t", "丄ê𐒖t"}, 136 | // String with all classes of codepoints above. 137 | {"\n丄\xed \x00\U0001FFFEa\uFFFD", "\n丄\uFFFD \uFFFD\uFFFDa\uFFFD"}, 138 | // Invalid byte sequence. 139 | {"\xff\x7e", "\uFFFD\x7e"}, 140 | // Single surrogate. 141 | // Go range clause advances one byte at a time on invalid UTF-8, 142 | // including codepoints that represent surrogates. 143 | {"\xED\xA0\x80", "\uFFFD\uFFFD\uFFFD"}, 144 | // Supplementary point U+233B4 encoded as a surrogate pair. 145 | {"\xED\xA1\x8C\xED\xBE\xB4", "\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD\uFFFD"}, 146 | // Overlong sequence. 147 | {"\xC0\x80", "\uFFFD\uFFFD"}, 148 | } { 149 | if coerced := coerceToUTF8InterchangeValid(tt.in); tt.expected != coerced { 150 | t.Errorf(`coerceToInterchangeValid(%q) == %q, want %q`, tt.in, coerced, tt.expected) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /identifier.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "fmt" 11 | "regexp" 12 | ) 13 | 14 | // A Identifier is an immutable string-like type that is safe to use in HTML 15 | // contexts as an identifier for HTML elements. For example, it is unsafe to 16 | // insert an untrusted string into a 17 | // 18 | // 19 | // 20 | // context since the string may be controlled by an attacker who can assign it 21 | // a value that masks existing DOM properties (i.e. DOM Clobbering). An 22 | // attacker may also be able to force legitimate Javascript code, which uses 23 | // document.getElementsByName(...) to read DOM elements, to refer to this 24 | // element. This may lead to unintended side effects, particularly if that 25 | // element contains attacker-controlled data. It is, however, safe to use an 26 | // Identifier in this context since its value is known to be partially or fully 27 | // under application control. 28 | // 29 | // In order to ensure that an attacker cannot influence the Identifier value, 30 | // an Identifier can only be instantiated from a compile-time constant string 31 | // literal prefix. 32 | // 33 | // Note that Identifier is Go-specific and therefore does not have a Proto form 34 | // for cross-language use. 35 | type Identifier struct { 36 | // We declare a Identifier not as a string but as a struct wrapping a string 37 | // to prevent construction of Identifier values through string conversion. 38 | str string 39 | } 40 | 41 | // To minimize the risk of parsing errors, Identifier values must start with an 42 | // alphabetical rune, and comprise of only alphanumeric, '-', and '_' runes. 43 | 44 | // startsWithAlphabetPattern matches strings that start with an alphabetical rune. 45 | var startsWithAlphabetPattern = regexp.MustCompile(`^[a-zA-Z]`) 46 | 47 | // onlyAlphanumericsOrHyphenPattern matches strings that only contain alphanumeric, 48 | // '-' and '_' runes. 49 | var onlyAlphanumericsOrHyphenPattern = regexp.MustCompile(`^[-_a-zA-Z0-9]*$`) 50 | 51 | // IdentifierFromConstant constructs an Identifier with its underlying identifier 52 | // set to the given string value, which must be an untyped string constant. It 53 | // panics if value does not start with an alphabetic rune or contains any 54 | // non-alphanumeric runes other than '-' and '_'. 55 | func IdentifierFromConstant(value stringConstant) Identifier { 56 | if !startsWithAlphabetPattern.MatchString(string(value)) || 57 | !onlyAlphanumericsOrHyphenPattern.MatchString(string(value)) { 58 | panic(fmt.Sprintf("invalid identifier %q", string(value))) 59 | } 60 | return Identifier{string(value)} 61 | } 62 | 63 | // IdentifierFromConstantPrefix constructs an Identifier with its underlying string 64 | // set to the string formed by joining prefix, which must be an untyped string 65 | // constant, and value with a hyphen. It panics if prefix or value contain any 66 | // non-alphanumeric runes other than '-' and '_', or if prefix does not start with 67 | // an alphabetic rune. 68 | func IdentifierFromConstantPrefix(prefix stringConstant, value string) Identifier { 69 | prefixString := string(prefix) 70 | if !startsWithAlphabetPattern.MatchString(string(prefix)) || 71 | !onlyAlphanumericsOrHyphenPattern.MatchString(string(prefix)) { 72 | panic(fmt.Sprintf("invalid prefix %q", string(prefix))) 73 | } 74 | if !onlyAlphanumericsOrHyphenPattern.MatchString(value) { 75 | panic(fmt.Sprintf("value %q contains non-alphanumeric runes", value)) 76 | } 77 | return Identifier{prefixString + "-" + value} 78 | } 79 | 80 | // String returns the string form of the Identifier. 81 | func (i Identifier) String() string { 82 | return i.str 83 | } 84 | -------------------------------------------------------------------------------- /identifier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "fmt" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestIdentifierFromConstant(t *testing.T) { 16 | tryIdentifierFromConstant := func(value string) (id Identifier, panicMsg string) { 17 | defer func() { 18 | r := recover() 19 | if r == nil { 20 | panicMsg = "" 21 | return 22 | } 23 | panicMsg = fmt.Sprint(r) 24 | }() 25 | return IdentifierFromConstant(stringConstant(value)), "" 26 | } 27 | 28 | for _, test := range [...]struct { 29 | value, panicMsg string 30 | }{ 31 | {"foo", ""}, 32 | {"F0ob4r", ""}, 33 | {"foo-bar", ""}, 34 | {"foo--bar", ""}, 35 | {"foo-bar-baz", ""}, 36 | {"foo-bar_baz", ""}, 37 | {"foo!", "invalid identifier"}, 38 | {"foo ", "invalid identifier"}, 39 | {"fo o", "invalid identifier"}, 40 | {" foo", "invalid identifier"}, 41 | {"foo\t", "invalid identifier"}, 42 | {"4wesome", "invalid identifier"}, 43 | } { 44 | id, panicMsg := tryIdentifierFromConstant(test.value) 45 | if test.panicMsg != "" { 46 | if !strings.Contains(panicMsg, test.panicMsg) { 47 | t.Errorf("value %q: got panic message:\n\t%q\nwant:\n\t%q", test.value, panicMsg, test.panicMsg) 48 | } 49 | continue 50 | } 51 | if panicMsg != "" { 52 | t.Errorf("value %q: unexpected panic: %q", test.value, panicMsg) 53 | continue 54 | } 55 | if got := id.String(); got != test.value { 56 | t.Errorf("value %q: got id: %q\twant: %q", test.value, got, test.value) 57 | } 58 | } 59 | } 60 | 61 | func TestIdentifierFromConstantPrefix(t *testing.T) { 62 | tryIdentifierFromConstantPrefix := func(prefix, value string) (id Identifier, panicMsg string) { 63 | defer func() { 64 | r := recover() 65 | if r == nil { 66 | panicMsg = "" 67 | return 68 | } 69 | panicMsg = fmt.Sprint(r) 70 | }() 71 | return IdentifierFromConstantPrefix(stringConstant(prefix), value), "" 72 | } 73 | 74 | for _, test := range [...]struct { 75 | prefix, value, want, panicMsg string 76 | }{ 77 | {"foo", "bar", "foo-bar", ""}, 78 | {"foo", "-bar", "foo--bar", ""}, 79 | {"foo", "bar-baz", "foo-bar-baz", ""}, 80 | {"foo", "bar-baz-", "foo-bar-baz-", ""}, 81 | {"foo", "bar_baz-", "foo-bar_baz-", ""}, 82 | {"foo", "", "foo-", ""}, 83 | {"", "bar", "", "invalid prefix"}, 84 | {"foo!", "bar", "", "invalid prefix"}, 85 | {"4wesome", "bar", "", "invalid prefix"}, 86 | {" foo", "bar", "", "invalid prefix"}, 87 | {"fo o", "bar", "", "invalid prefix"}, 88 | {"foo ", "bar", "", "invalid prefix"}, 89 | {"foo\t", "bar", "", "invalid prefix"}, 90 | {"foo\n", "bar", "", "invalid prefix"}, 91 | {"\nfoo", "bar", "", "invalid prefix"}, 92 | {"foo", "bar!", "", "contains non-alphanumeric runes"}, 93 | {"foo", " bar", "", "contains non-alphanumeric runes"}, 94 | {"foo", "b ar", "", "contains non-alphanumeric runes"}, 95 | {"foo", "bar ", "", "contains non-alphanumeric runes"}, 96 | {"foo", "bar\t", "", "contains non-alphanumeric runes"}, 97 | } { 98 | id, panicMsg := tryIdentifierFromConstantPrefix(test.prefix, test.value) 99 | inputs := fmt.Sprintf("prefix %q, value %q", test.prefix, test.value) 100 | if test.panicMsg != "" { 101 | if !strings.Contains(panicMsg, test.panicMsg) { 102 | t.Errorf("%s: got panic message:\n\t%q\nwant:\n\t%q", inputs, panicMsg, test.panicMsg) 103 | } 104 | continue 105 | } 106 | if panicMsg != "" { 107 | t.Errorf("%s: unexpected panic: %q", inputs, panicMsg) 108 | continue 109 | } 110 | if got := id.String(); got != test.want { 111 | t.Errorf("%s: got id: %q\twant: %q", inputs, got, test.want) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "github.com/google/safehtml/internal/raw" 11 | ) 12 | 13 | // stringConstant is an unexported string type. Users of this package cannot 14 | // create values of this type except by passing an untyped string constant to 15 | // functions which expect a stringConstant. This type should only be used in 16 | // function and method parameters. 17 | type stringConstant string 18 | 19 | // The following functions are used by package uncheckedconversions 20 | // (via package raw) to create safe HTML types from plain strings. 21 | 22 | func htmlRaw(s string) HTML { 23 | return HTML{s} 24 | } 25 | 26 | func scriptRaw(s string) Script { 27 | return Script{s} 28 | } 29 | 30 | func style(s string) Style { 31 | return Style{s} 32 | } 33 | 34 | func styleSheetRaw(s string) StyleSheet { 35 | return StyleSheet{s} 36 | } 37 | 38 | func urlRaw(s string) URL { 39 | return URL{s} 40 | } 41 | 42 | func trustedResourceURLRaw(s string) TrustedResourceURL { 43 | return TrustedResourceURL{s} 44 | } 45 | 46 | func identifierRaw(s string) Identifier { 47 | return Identifier{s} 48 | } 49 | 50 | func init() { 51 | raw.HTML = htmlRaw 52 | raw.Script = scriptRaw 53 | raw.Style = style 54 | raw.StyleSheet = styleSheetRaw 55 | raw.URL = urlRaw 56 | raw.TrustedResourceURL = trustedResourceURLRaw 57 | raw.Identifier = identifierRaw 58 | } 59 | -------------------------------------------------------------------------------- /internal/raw/raw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // Package raw provides a coordination point for package safehtml, package 8 | // uncheckedconversions, package legacyconversions, and package testconversions. 9 | // raw must only be imported by these four packages. 10 | package raw 11 | 12 | // HTML is the raw constructor for a safehtml.HTML. 13 | var HTML interface{} 14 | 15 | // Script is the raw constructor for a safehtml.Script. 16 | var Script interface{} 17 | 18 | // Style is the raw constructor for a safehtml.Style. 19 | var Style interface{} 20 | 21 | // StyleSheet is the raw constructor for a safehtml.StyleSheet. 22 | var StyleSheet interface{} 23 | 24 | // URL is the raw constructor for a safehtml.URL. 25 | var URL interface{} 26 | 27 | // TrustedResourceURL is the raw constructor for a safehtml.TrustedResourceURL. 28 | var TrustedResourceURL interface{} 29 | 30 | // Identifier is the raw constructor for a safehtml.Identifier. 31 | var Identifier interface{} 32 | -------------------------------------------------------------------------------- /internal/safehtmlutil/safehtmlutil.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // Package safehtmlutil contains functions shared by package safehtml and safehtml/template. 8 | package safehtmlutil 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "reflect" 14 | "regexp" 15 | ) 16 | 17 | // IsSafeTrustedResourceURLPrefix returns whether the given prefix is safe to use as a 18 | // TrustedResourceURL prefix. 19 | // 20 | // TrustedResourceURL prefixes must start with one of the following: 21 | // - `https:///` 22 | // - `///` 23 | // - `/` 24 | // - `about:blank#` 25 | // 26 | // `` must contain only alphanumerics, '.', ':', '[', ']', or '-'. 27 | // These restrictions do not enforce a well-formed domain name, so '.' and '1.2' are valid. 28 | // 29 | // `` is any character except `/` and `\`. Based on 30 | // https://url.spec.whatwg.org/commit-snapshots/56b74ce7cca8883eab62e9a12666e2fac665d03d/#url-parsing, 31 | // an initial / which is not followed by another / or \ will end up in the "path state" and from there 32 | // it can only go to the "fragment state" and "query state". 33 | func IsSafeTrustedResourceURLPrefix(prefix string) bool { 34 | return safeTrustedResourceURLPrefixPattern.MatchString(prefix) 35 | } 36 | 37 | var safeTrustedResourceURLPrefixPattern = regexp.MustCompile(`(?i)^(?:` + 38 | `(?:https:)?//[0-9a-z.:\[\]-]+/|` + 39 | `/[^/\\]|` + 40 | `about:blank#)`) 41 | 42 | // URLContainsDoubleDotSegment returns whether the given URL or URL substring 43 | // contains the double dot-segment ".." (RFC3986 3.3) in its percent-encoded or 44 | // unencoded form. 45 | func URLContainsDoubleDotSegment(url string) bool { 46 | return urlDoubleDotSegmentPattern.MatchString(url) 47 | } 48 | 49 | var urlDoubleDotSegmentPattern = regexp.MustCompile(`(?i)(?:\.|%2e)(?:\.|%2e)`) 50 | 51 | // QueryEscapeURL produces an output that can be embedded in a URL query. 52 | // The output can be embedded in an HTML attribute without further escaping. 53 | func QueryEscapeURL(args ...interface{}) string { 54 | return urlProcessor(false, Stringify(args...)) 55 | } 56 | 57 | // NormalizeURL normalizes URL content so it can be embedded in a quote-delimited 58 | // string or parenthesis delimited url(...). 59 | // The normalizer does not encode all HTML specials. Specifically, it does not 60 | // encode '&' so correct embedding in an HTML attribute requires escaping of 61 | // '&' to '&'. 62 | func NormalizeURL(args ...interface{}) string { 63 | return urlProcessor(true, Stringify(args...)) 64 | } 65 | 66 | // urlProcessor normalizes (when norm is true) or escapes its input to produce 67 | // a valid hierarchical or opaque URL part. 68 | func urlProcessor(norm bool, s string) string { 69 | var b bytes.Buffer 70 | written := 0 71 | // The byte loop below assumes that all URLs use UTF-8 as the 72 | // content-encoding. This is similar to the URI to IRI encoding scheme 73 | // defined in section 3.1 of RFC 3987, and behaves the same as the 74 | // EcmaScript builtin encodeURIComponent. 75 | // It should not cause any misencoding of URLs in pages with 76 | // Content-type: text/html;charset=UTF-8. 77 | for i, n := 0, len(s); i < n; i++ { 78 | c := s[i] 79 | switch c { 80 | // Single quote and parens are sub-delims in RFC 3986, but we 81 | // escape them so the output can be embedded in single 82 | // quoted attributes and unquoted CSS url(...) constructs. 83 | // Single quotes are reserved in URLs, but are only used in 84 | // the obsolete "mark" rule in an appendix in RFC 3986 85 | // so can be safely encoded. 86 | case '!', '#', '$', '&', '*', '+', ',', '/', ':', ';', '=', '?', '@', '[', ']': 87 | if norm { 88 | continue 89 | } 90 | // Unreserved according to RFC 3986 sec 2.3 91 | // "For consistency, percent-encoded octets in the ranges of 92 | // ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D), 93 | // period (%2E), underscore (%5F), or tilde (%7E) should not be 94 | // created by URI producers 95 | case '-', '.', '_', '~': 96 | continue 97 | case '%': 98 | // When normalizing do not re-encode valid escapes. 99 | if norm && i+2 < len(s) && isHex(s[i+1]) && isHex(s[i+2]) { 100 | continue 101 | } 102 | default: 103 | // Unreserved according to RFC 3986 sec 2.3 104 | if 'a' <= c && c <= 'z' { 105 | continue 106 | } 107 | if 'A' <= c && c <= 'Z' { 108 | continue 109 | } 110 | if '0' <= c && c <= '9' { 111 | continue 112 | } 113 | } 114 | b.WriteString(s[written:i]) 115 | fmt.Fprintf(&b, "%%%02x", c) 116 | written = i + 1 117 | } 118 | if written == 0 { 119 | return s 120 | } 121 | b.WriteString(s[written:]) 122 | return b.String() 123 | } 124 | 125 | // isHex reports whether the given character is a hex digit. 126 | func isHex(c byte) bool { 127 | return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' 128 | } 129 | 130 | // Stringify converts its arguments to a string. It is equivalent to 131 | // fmt.Sprint(args...), except that it deferences all pointers. 132 | func Stringify(args ...interface{}) string { 133 | // Optimization for simple common case of a single string argument. 134 | if len(args) == 1 { 135 | if s, ok := args[0].(string); ok { 136 | return s 137 | } 138 | } 139 | for i, arg := range args { 140 | args[i] = indirectToStringerOrError(arg) 141 | } 142 | return fmt.Sprint(args...) 143 | } 144 | 145 | var ( 146 | errorType = reflect.TypeOf((*error)(nil)).Elem() 147 | fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() 148 | ) 149 | 150 | // indirectToStringerOrError dereferences a as many times 151 | // as necessary to reach the base type, an implementation of fmt.Stringer, 152 | // or an implementation of error, and returns a value of that type. It returns 153 | // nil if a is nil. 154 | func indirectToStringerOrError(a interface{}) interface{} { 155 | if a == nil { 156 | return nil 157 | } 158 | v := reflect.ValueOf(a) 159 | for !v.Type().Implements(fmtStringerType) && !v.Type().Implements(errorType) && v.Kind() == reflect.Ptr && !v.IsNil() { 160 | v = v.Elem() 161 | } 162 | return v.Interface() 163 | } 164 | 165 | // Indirect returns the value, after dereferencing as many times 166 | // as necessary to reach the base type (or nil). 167 | func Indirect(a interface{}) interface{} { 168 | if a == nil { 169 | return nil 170 | } 171 | if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr { 172 | // Avoid creating a reflect.Value if it's not a pointer. 173 | return a 174 | } 175 | v := reflect.ValueOf(a) 176 | for v.Kind() == reflect.Ptr && !v.IsNil() { 177 | v = v.Elem() 178 | } 179 | return v.Interface() 180 | } 181 | -------------------------------------------------------------------------------- /internal/safehtmlutil/safehtmlutil_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtmlutil 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestIsSafeTrustedResourceURLPrefix(t *testing.T) { 14 | for _, test := range [...]struct { 15 | in string 16 | want bool 17 | }{ 18 | // With scheme. 19 | {`httpS://www.foO.com/`, true}, 20 | // Scheme-relative. 21 | {`//www.foo.com/`, true}, 22 | // Origin with hypen and port. 23 | {`//ww-w.foo.com:1000/path`, true}, 24 | // IPv6 origin. 25 | {`//[::1]/path`, true}, 26 | // Path-absolute. 27 | {`/path`, true}, 28 | {`/path/x`, true}, 29 | {`/path#x`, true}, 30 | {`/path?x`, true}, 31 | // Mixed case. 32 | {`httpS://www.foo.cOm/pAth`, true}, 33 | {`about:blank#`, true}, 34 | {`about:blank#x`, true}, 35 | // Scheme prefix. 36 | {`j`, false}, 37 | {`java`, false}, 38 | {`on`, false}, 39 | {`data-`, false}, 40 | // Unsafe scheme 41 | {`javascript:`, false}, 42 | {`javascript:alert`, false}, 43 | // Invalid scheme. 44 | {`ftp://`, false}, 45 | // Missing origin. 46 | {`https://`, false}, 47 | {`https:///`, false}, // NOTYPO 48 | {`//`, false}, 49 | {`///`, false}, 50 | // Missing / after origin. 51 | {`https://foo.com`, false}, 52 | // Invalid char in origin. 53 | {`https://www.foo%.com/`, false}, 54 | {`https://www.foo\\.com/`, false}, 55 | {`https://user:password@www.foo.com/`, false}, 56 | // Two slashes, would allow origin to be set dynamically. 57 | {`//`, false}, 58 | // Two slashes. IE allowed (allows?) '\' instead of '/'. 59 | {`/\\`, false}, 60 | // Relative path. 61 | {`abc`, false}, 62 | {`about:blank`, false}, 63 | {`about:blankX`, false}, 64 | } { 65 | if got := IsSafeTrustedResourceURLPrefix(test.in); got != test.want { 66 | t.Errorf("IsSafeTrustedResourceURLPrefix(%q) = %t", test.in, got) 67 | } 68 | } 69 | } 70 | 71 | func TestURLContainsDoubleDotSegment(t *testing.T) { 72 | for _, test := range [...]struct { 73 | in string 74 | want bool 75 | }{ 76 | // Permutations of double dot-segment URL substrings. 77 | {`..`, true}, 78 | {`%2e%2e`, true}, 79 | {`%2E%2e`, true}, 80 | {`%2e%2E`, true}, 81 | {`%2E%2E`, true}, 82 | {`.%2e`, true}, 83 | {`.%2E`, true}, 84 | {`%2e.`, true}, 85 | {`%2E.`, true}, 86 | // Permutations of single dot-segments URL substrings. 87 | {`.`, false}, 88 | {`%2e`, false}, 89 | {`%2E`, false}, 90 | // These URL substrings do not technically denote dot-segments, but are very 91 | // unlikely to be part of a legitimate URL. 92 | {`foo..`, true}, 93 | {`..foo`, true}, 94 | // Non-contiguous dots 95 | {`.foo.`, false}, 96 | // Full URLs with a sampling of the double and single dot-segment substrings 97 | // from the previous test cases. 98 | {`http://www.test.com/../bar`, true}, 99 | {`http://www.test.com/foo../bar`, true}, 100 | {`http://www.test.com/bar/%2E%2e`, true}, 101 | {`http://www.test.com/./bar`, false}, 102 | {`http://www.test.com/.foo./bar`, false}, 103 | {`http://www.test.com/bar/%2E`, false}, 104 | } { 105 | if got := URLContainsDoubleDotSegment(test.in); got != test.want { 106 | t.Errorf("URLContainsDoubleDotSegment(%q) = %t", test.in, got) 107 | } 108 | } 109 | } 110 | 111 | func TestNormalizeURL(t *testing.T) { 112 | for _, test := range [...]struct { 113 | url, want string 114 | }{ 115 | {"", ""}, 116 | { 117 | "http://example.com:80/foo/bar?q=foo%20&bar=x+y#frag", 118 | "http://example.com:80/foo/bar?q=foo%20&bar=x+y#frag", 119 | }, 120 | {" ", "%20"}, 121 | {"%7c", "%7c"}, 122 | {"%7C", "%7C"}, 123 | {"%2", "%252"}, 124 | {"%", "%25"}, 125 | {"%z", "%25z"}, 126 | {"/foo|bar/%5c\u1234", "/foo%7cbar/%5c%e1%88%b4"}, 127 | } { 128 | if got := NormalizeURL(test.url); test.want != got { 129 | t.Errorf("%q: got\n\t%q\n, want\n\t%q", test.url, got, test.want) 130 | } 131 | if test.want != NormalizeURL(test.want) { 132 | t.Errorf("not idempotent: %q", test.want) 133 | } 134 | } 135 | } 136 | 137 | func TestQueryEscapeURL(t *testing.T) { 138 | const input = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f" + 139 | "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + 140 | ` !"#$%&'()*+,-./` + 141 | `0123456789:;<=>?` + 142 | `@ABCDEFGHIJKLMNO` + 143 | `PQRSTUVWXYZ[\]^_` + 144 | "`abcdefghijklmno" + 145 | "pqrstuvwxyz{|}~\x7f" + 146 | "\u00A0\u0100\u2028\u2029\ufeff\U0001D11E" 147 | const want = "%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f" + 148 | "%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f" + 149 | "%20%21%22%23%24%25%26%27%28%29%2a%2b%2c-.%2f" + 150 | "0123456789%3a%3b%3c%3d%3e%3f" + 151 | "%40ABCDEFGHIJKLMNO" + 152 | "PQRSTUVWXYZ%5b%5c%5d%5e_" + 153 | "%60abcdefghijklmno" + 154 | "pqrstuvwxyz%7b%7c%7d~%7f" + 155 | "%c2%a0%c4%80%e2%80%a8%e2%80%a9%ef%bb%bf%f0%9d%84%9e" 156 | if got := QueryEscapeURL(input); want != got { 157 | t.Fatalf("got\n\t%q\nwant\n\t%q", got, want) 158 | } 159 | } 160 | 161 | func BenchmarkQueryEscapeURL(b *testing.B) { 162 | for i := 0; i < b.N; i++ { 163 | QueryEscapeURL("http://example.com:80/foo?q=bar%20&baz=x+y#frag") 164 | } 165 | } 166 | 167 | func BenchmarkQueryEscapeURLNoSpecials(b *testing.B) { 168 | for i := 0; i < b.N; i++ { 169 | QueryEscapeURL("TheQuickBrownFoxJumpsOverTheLazyDog.") 170 | } 171 | } 172 | 173 | func BenchmarkNormalizeURL(b *testing.B) { 174 | for i := 0; i < b.N; i++ { 175 | NormalizeURL("The quick brown fox jumps over the lazy dog.\n") 176 | } 177 | } 178 | 179 | func BenchmarkNormalizeURLNoSpecials(b *testing.B) { 180 | for i := 0; i < b.N; i++ { 181 | NormalizeURL("http://example.com:80/foo?q=bar%20&baz=x+y#frag") 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /internal/template/raw/raw.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // Package raw provides a coordination point for package safehtml/template and 8 | // package safehtml/template/uncheckedconversions. raw must be imported only by 9 | // these two packages. 10 | package raw 11 | 12 | // TrustedSource is the raw constructor for a template.TrustedSource. 13 | var TrustedSource interface{} 14 | 15 | // TrustedTemplate is the raw constructor for a template.TrustedTemplate. 16 | var TrustedTemplate interface{} 17 | -------------------------------------------------------------------------------- /legacyconversions/legacyconversions.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // Package legacyconversions provides functions to create values of package 8 | // safehtml types from plain strings. This package is functionally equivalent 9 | // to package uncheckedconversions, but is only intended for temporary use 10 | // when upgrading code to use package safehtml types. 11 | // 12 | // New code must not use the conversion functions in this package. Instead, new code 13 | // should create package safehtml type values using the functions provided in package 14 | // safehtml or package safehtml/template. If neither of these options are feasible, 15 | // new code should request a security review to use the conversion functions in package 16 | // safehtml/uncheckedconversions instead. 17 | package legacyconversions 18 | 19 | import ( 20 | "github.com/google/safehtml/internal/raw" 21 | "github.com/google/safehtml" 22 | ) 23 | 24 | var html = raw.HTML.(func(string) safehtml.HTML) 25 | var script = raw.Script.(func(string) safehtml.Script) 26 | var style = raw.Style.(func(string) safehtml.Style) 27 | var styleSheet = raw.StyleSheet.(func(string) safehtml.StyleSheet) 28 | var url = raw.URL.(func(string) safehtml.URL) 29 | var trustedResourceURL = raw.TrustedResourceURL.(func(string) safehtml.TrustedResourceURL) 30 | var identifier = raw.Identifier.(func(string) safehtml.Identifier) 31 | 32 | // RiskilyAssumeHTML converts a plain string into a HTML. 33 | // This function must only be used for refactoring legacy code. 34 | func RiskilyAssumeHTML(s string) safehtml.HTML { 35 | return html(s) 36 | } 37 | 38 | // RiskilyAssumeScript converts a plain string into a Script. 39 | // This function must only be used for refactoring legacy code. 40 | func RiskilyAssumeScript(s string) safehtml.Script { 41 | return script(s) 42 | } 43 | 44 | // RiskilyAssumeStyle converts a plain string into a Style. 45 | // This function must only be used for refactoring legacy code. 46 | func RiskilyAssumeStyle(s string) safehtml.Style { 47 | return style(s) 48 | } 49 | 50 | // RiskilyAssumeStyleSheet converts a plain string into a StyleSheet. 51 | // This function must only be used for refactoring legacy code. 52 | func RiskilyAssumeStyleSheet(s string) safehtml.StyleSheet { 53 | return styleSheet(s) 54 | } 55 | 56 | // RiskilyAssumeURL converts a plain string into a URL. 57 | // This function must only be used for refactoring legacy code. 58 | func RiskilyAssumeURL(s string) safehtml.URL { 59 | return url(s) 60 | } 61 | 62 | // RiskilyAssumeTrustedResourceURL converts a plain string into a TrustedResourceURL. 63 | // This function must only be used for refactoring legacy code. 64 | func RiskilyAssumeTrustedResourceURL(s string) safehtml.TrustedResourceURL { 65 | return trustedResourceURL(s) 66 | } 67 | 68 | // RiskilyAssumeIdentifier converts a plain string into an Identifier. 69 | // This function must only be used for refactoring legacy code. 70 | func RiskilyAssumeIdentifier(s string) safehtml.Identifier { 71 | return identifier(s) 72 | } 73 | -------------------------------------------------------------------------------- /script.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "regexp" 13 | ) 14 | 15 | // A Script is an immutable string-like type which represents JavaScript 16 | // code and guarantees that its value, as a string, will not cause execution 17 | // of unconstrained attacker controlled code (cross-site scripting) when 18 | // evaluated as JavaScript in a browser. 19 | // 20 | // Script's string representation can safely be interpolated as the 21 | // content of a script element within HTML, and can safely be passed to DOM 22 | // properties and functions which expect JavaScript. In these cases, the Script 23 | // string should not be escaped. Script's string representation can also be safely 24 | // used as the value for on* attribute handlers in HTML, though the Script string 25 | // must be escaped before such use. 26 | // 27 | // Note that the Script might contain text that is attacker-controlled but 28 | // that text should have been interpolated with appropriate escaping, 29 | // sanitization and/or validation into the right location in the script, such 30 | // that it is highly constrained in its effect (for example, it had to match a 31 | // set of allowed words). 32 | // 33 | // In order to ensure that an attacker cannot influence the Script 34 | // value, a Script can only be instantiated from compile-time 35 | // constant string literals or security-reviewed unchecked conversions, 36 | // but never from arbitrary string values potentially representing untrusted 37 | // user input. 38 | type Script struct { 39 | // We declare a Script not as a string but as a struct wrapping a string 40 | // to prevent construction of Script values through string conversion. 41 | str string 42 | } 43 | 44 | // ScriptFromConstant constructs a Script with its underlying script set 45 | // to the given script, which must be an untyped string constant. 46 | // 47 | // No runtime validation or sanitization is performed on script; being under 48 | // application control, it is simply assumed to comply with the Script 49 | // contract. 50 | func ScriptFromConstant(script stringConstant) Script { 51 | return Script{string(script)} 52 | } 53 | 54 | // ScriptFromDataAndConstant constructs a Script of the form 55 | // 56 | // var name = data; script 57 | // 58 | // where name is the supplied variable name, data is the supplied data value 59 | // encoded as JSON using encoding/json.Marshal, and script is the supplied 60 | // JavaScript statement or sequence of statements. The supplied name and script 61 | // must both be untyped string constants. It returns an error if name is not a 62 | // valid Javascript identifier or JSON encoding fails. 63 | // 64 | // No runtime validation or sanitization is performed on script; being under 65 | // application control, it is simply assumed to comply with the Script 66 | // contract. 67 | func ScriptFromDataAndConstant(name stringConstant, data interface{}, script stringConstant) (Script, error) { 68 | if !jsIdentifierPattern.MatchString(string(name)) { 69 | return Script{}, fmt.Errorf("variable name %q is an invalid Javascript identifier", string(name)) 70 | } 71 | json, err := json.Marshal(data) 72 | if err != nil { 73 | return Script{}, err 74 | } 75 | return Script{fmt.Sprintf("var %s = %s;\n%s", name, json, string(script))}, nil 76 | } 77 | 78 | // jsIdentifierPattern matches strings that are valid Javascript identifiers. 79 | // 80 | // This pattern accepts only a subset of valid identifiers defined in 81 | // https://tc39.github.io/ecma262/#sec-names-and-keywords. In particular, 82 | // it does not match identifiers that contain non-ASCII letters, Unicode 83 | // escape sequences, and the Unicode format-control characters 84 | // \u200C (zero-width non-joiner) and \u200D (zero-width joiner). 85 | var jsIdentifierPattern = regexp.MustCompile(`^[$_a-zA-Z][$_a-zA-Z0-9]+$`) 86 | 87 | // String returns the string form of the Script. 88 | func (s Script) String() string { 89 | return s.str 90 | } 91 | -------------------------------------------------------------------------------- /script_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestScriptFromDataAndConstant(t *testing.T) { 15 | testStruct := struct { 16 | ID int 17 | Name string 18 | Data []string 19 | }{ 20 | ID: 3, 21 | Name: "Animals", 22 | Data: []string{"Cats", "Dogs", "Hamsters"}, 23 | } 24 | for _, test := range [...]struct { 25 | desc string 26 | name stringConstant 27 | data interface{} 28 | script stringConstant 29 | want, err string 30 | }{ 31 | { 32 | "string data with HTML special characters", 33 | `myVar`, 34 | ``, 35 | `alert(myVar);`, 36 | `var myVar = "\u003c/script\u003e"; 37 | alert(myVar);`, "", 38 | }, 39 | { 40 | "output of custom JSON marshaler escaped", 41 | `myVar`, 42 | dataWithUnsafeMarshaler(`""`), 43 | `alert(myVar);`, 44 | `var myVar = "\u003c/script\u003e"; 45 | alert(myVar);`, "", 46 | }, 47 | { 48 | "invalid output of custom JSON marshaler rejected", 49 | `myVar`, 50 | dataWithUnsafeMarshaler(`"hello"; alert(1)`), 51 | `alert(myVar);`, 52 | "", "json: error calling MarshalJSON for type safehtml.dataWithUnsafeMarshaler", 53 | }, 54 | { 55 | "struct data", 56 | `myVar`, 57 | testStruct, 58 | `alert(myVar);`, 59 | `var myVar = {"ID":3,"Name":"Animals","Data":["Cats","Dogs","Hamsters"]}; 60 | alert(myVar);`, "", 61 | }, 62 | { 63 | "multi-line script", 64 | `myVar`, 65 | ``, 66 | `alert(myVar); 67 | alert("hello world!");`, 68 | `var myVar = "\u003cfoo\u003e"; 69 | alert(myVar); 70 | alert("hello world!");`, "", 71 | }, 72 | { 73 | "empty variable name", 74 | "", 75 | ``, 76 | `alert(myVar);`, 77 | "", `variable name "" is an invalid Javascript identifier`, 78 | }, 79 | { 80 | "invalid variable name", 81 | "café", 82 | ``, 83 | `alert(myVar);`, 84 | "", `variable name "café" is an invalid Javascript identifier`, 85 | }, 86 | { 87 | "JSON encoding error", 88 | `myVar`, 89 | make(chan int), 90 | `alert(myVar);`, 91 | "", "json: unsupported type: chan int", 92 | }, 93 | } { 94 | s, err := ScriptFromDataAndConstant(test.name, test.data, test.script) 95 | if test.err != "" && err == nil { 96 | t.Errorf("%s : expected error", test.desc) 97 | } else if test.err != "" && !strings.Contains(err.Error(), test.err) { 98 | t.Errorf("%s : got error:\n\t%s\nwant error:\n\t%s", test.desc, err, test.err) 99 | } else if test.err == "" && err != nil { 100 | t.Errorf("%s : unexpected error: %s", test.desc, err) 101 | } else if got := s.String(); got != test.want { 102 | t.Errorf("%s : got:\n%s\nwant:\n%s", test.desc, got, test.want) 103 | } 104 | } 105 | } 106 | 107 | type dataWithUnsafeMarshaler string 108 | 109 | func (d dataWithUnsafeMarshaler) MarshalJSON() ([]byte, error) { 110 | return []byte(string(d)), nil 111 | } 112 | 113 | func TestJSIdentifierPattern(t *testing.T) { 114 | for _, test := range [...]struct { 115 | in string 116 | want bool 117 | }{ 118 | {`foo`, true}, 119 | {`Foo`, true}, 120 | {`f0o`, true}, 121 | {`_f0o`, true}, 122 | {`$f0o`, true}, 123 | {`f0$_o`, true}, 124 | {`_f0$_o`, true}, 125 | // Starts with digit. 126 | {`2foo`, false}, 127 | // Contains alphabetic codepoints that are not ASCII letters. 128 | {`café`, false}, 129 | {`Χαίρετε`, false}, 130 | // Contains non-alphabetic codepoints. 131 | {`你好`, false}, 132 | // Contains unicode escape sequences. 133 | {`\u0192oo`, false}, 134 | {`f\u006Fo`, false}, 135 | // Contains zero-width non-joiner. 136 | {"dea\u200Cly", false}, 137 | // Contains zero-width joiner. 138 | {"क्\u200D", false}, 139 | } { 140 | if got := jsIdentifierPattern.MatchString(test.in); got != test.want { 141 | t.Errorf("jsIdentifierPattern.MatchString(%q) = %t", test.in, got) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /style_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "fmt" 11 | "reflect" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func TestStyleFromConstantPanic(t *testing.T) { 17 | for _, test := range [...]struct { 18 | desc, input, want string 19 | }{ 20 | { 21 | desc: "angle brackets 1", 22 | input: `width: x<;`, 23 | want: `contains angle brackets`, 24 | }, 25 | { 26 | desc: "angle brackets 2", 27 | input: `width: x>;`, 28 | want: `contains angle brackets`, 29 | }, 30 | { 31 | desc: "angle brackets 3", 32 | input: ``, 33 | want: `contains angle brackets`, 34 | }, 35 | { 36 | desc: "no ending semicolon", 37 | input: `width: 1em`, 38 | want: `must end with ';'`, 39 | }, 40 | { 41 | desc: "no colon", 42 | input: `width= 1em;`, 43 | want: `must contain at least one ':' to specify a property-value pair`, 44 | }, 45 | } { 46 | tryStyleFromConstant := func() (ret string) { 47 | defer func() { 48 | r := recover() 49 | if r == nil { 50 | ret = "" 51 | return 52 | } 53 | ret = fmt.Sprint(r) 54 | }() 55 | StyleFromConstant(stringConstant(test.input)) 56 | return "" 57 | } 58 | errMsg := tryStyleFromConstant() 59 | if errMsg == "" { 60 | t.Errorf("%s: expected panic", test.desc) 61 | continue 62 | } 63 | if !strings.Contains(errMsg, test.want) { 64 | t.Errorf("%s: error message does not contain\n\t%q\ngot:\n\t%q", test.desc, test.want, errMsg) 65 | } 66 | } 67 | } 68 | 69 | func TestStyleFromProperties(t *testing.T) { 70 | for _, test := range [...]struct { 71 | desc string 72 | input StyleProperties 73 | want string 74 | }{ 75 | { 76 | desc: "BackgroundImageURLs single URL", 77 | input: StyleProperties{ 78 | BackgroundImageURLs: []string{"http://goodUrl.com/a"}, 79 | }, 80 | want: `background-image:url("http://goodUrl.com/a");`, 81 | }, 82 | { 83 | desc: "BackgroundImageURLs multiple URLs", 84 | input: StyleProperties{ 85 | BackgroundImageURLs: []string{"http://goodUrl.com/a", "http://goodUrl.com/b"}, 86 | }, 87 | want: `background-image:url("http://goodUrl.com/a"), url("http://goodUrl.com/b");`, 88 | }, 89 | { 90 | desc: "BackgroundImageURLs invalid runes in URL escaped", 91 | input: StyleProperties{ 92 | BackgroundImageURLs: []string{"http://goodUrl.com/a\"\\\n"}, 93 | }, 94 | want: `background-image:url("http://goodUrl.com/a\000022\00005C\00000A");`, 95 | }, 96 | { 97 | desc: "FontFamily unquoted names", 98 | input: StyleProperties{ 99 | FontFamily: []string{"serif", "sans-serif", "GulimChe"}, 100 | }, 101 | want: `font-family:serif, sans-serif, GulimChe;`, 102 | }, 103 | { 104 | desc: "FontFamily quoted names", 105 | input: StyleProperties{ 106 | FontFamily: []string{"\nserif", "serif\n", "Goudy Bookletter 1911", "New Century Schoolbook", `"sans-serif"`}, 107 | }, 108 | want: `font-family:"\00000Aserif", "serif\00000A", "Goudy Bookletter 1911", "New Century Schoolbook", "sans-serif";`, 109 | }, 110 | { 111 | desc: "FontFamily quoted and unquoted names", 112 | input: StyleProperties{ 113 | FontFamily: []string{"sans-serif", "Goudy Bookletter 1911", "GulimChe", `"fantasy"`, "Times New Roman"}, 114 | }, 115 | want: `font-family:sans-serif, "Goudy Bookletter 1911", GulimChe, "fantasy", "Times New Roman";`, 116 | }, 117 | { 118 | desc: "Display", 119 | input: StyleProperties{ 120 | Display: "inline", 121 | }, 122 | want: "display:inline;", 123 | }, 124 | { 125 | desc: "BackgroundColor", 126 | input: StyleProperties{ 127 | BackgroundColor: "red", 128 | }, 129 | want: "background-color:red;", 130 | }, 131 | { 132 | desc: "BackgroundPosition", 133 | input: StyleProperties{ 134 | BackgroundPosition: "100px -110px", 135 | }, 136 | want: "background-position:100px -110px;", 137 | }, 138 | { 139 | desc: "BackgroundRepeat", 140 | input: StyleProperties{ 141 | BackgroundRepeat: "no-repeat", 142 | }, 143 | want: "background-repeat:no-repeat;", 144 | }, 145 | { 146 | desc: "BackgroundSize", 147 | input: StyleProperties{ 148 | BackgroundSize: "10px", 149 | }, 150 | want: "background-size:10px;", 151 | }, 152 | { 153 | desc: "Color", 154 | input: StyleProperties{ 155 | Color: "#000", 156 | }, 157 | want: "color:#000;", 158 | }, 159 | { 160 | desc: "Height", 161 | input: StyleProperties{ 162 | Height: "100px", 163 | }, 164 | want: "height:100px;", 165 | }, 166 | { 167 | desc: "Width", 168 | input: StyleProperties{ 169 | Width: "120px", 170 | }, 171 | want: "width:120px;", 172 | }, 173 | { 174 | desc: "Left", 175 | input: StyleProperties{ 176 | Left: "140px", 177 | }, 178 | want: "left:140px;", 179 | }, 180 | { 181 | desc: "Right", 182 | input: StyleProperties{ 183 | Right: "160px", 184 | }, 185 | want: "right:160px;", 186 | }, 187 | { 188 | desc: "Top", 189 | input: StyleProperties{ 190 | Top: "180px", 191 | }, 192 | want: "top:180px;", 193 | }, 194 | { 195 | desc: "Bottom", 196 | input: StyleProperties{ 197 | Bottom: "200px", 198 | }, 199 | want: "bottom:200px;", 200 | }, 201 | { 202 | desc: "FontWeight", 203 | input: StyleProperties{ 204 | FontWeight: "100", 205 | }, 206 | want: "font-weight:100;", 207 | }, 208 | { 209 | desc: "Padding", 210 | input: StyleProperties{ 211 | Padding: "5px 1em 0 2em", 212 | }, 213 | want: "padding:5px 1em 0 2em;", 214 | }, 215 | { 216 | desc: "ZIndex", 217 | input: StyleProperties{ 218 | ZIndex: "-2", 219 | }, 220 | want: "z-index:-2;", 221 | }, 222 | { 223 | desc: "multiple properties", 224 | input: StyleProperties{ 225 | BackgroundImageURLs: []string{"http://goodUrl.com/a", "http://goodUrl.com/b"}, 226 | FontFamily: []string{"serif", "Goudy Bookletter 1911", "Times New Roman", "monospace"}, 227 | BackgroundColor: "#bbff10", 228 | BackgroundPosition: "100px -110px", 229 | BackgroundRepeat: "no-repeat", 230 | BackgroundSize: "10px", 231 | Width: "12px", 232 | Height: "10px", 233 | }, 234 | want: `background-image:url("http://goodUrl.com/a"), url("http://goodUrl.com/b");` + 235 | `font-family:serif, "Goudy Bookletter 1911", "Times New Roman", monospace;` + 236 | `background-color:#bbff10;` + 237 | `background-position:100px -110px;` + 238 | `background-repeat:no-repeat;` + 239 | `background-size:10px;` + 240 | `height:10px;` + 241 | `width:12px;`, 242 | }, 243 | { 244 | desc: "multiple properties, some empty and unset", 245 | input: StyleProperties{ 246 | BackgroundImageURLs: []string{"http://goodUrl.com/a", "http://goodUrl.com/b"}, 247 | BackgroundPosition: "100px -110px", 248 | BackgroundSize: "", 249 | Width: "12px", 250 | Height: "10px", 251 | }, 252 | want: `background-image:url("http://goodUrl.com/a"), url("http://goodUrl.com/b");` + 253 | `background-position:100px -110px;` + 254 | `height:10px;` + 255 | `width:12px;`, 256 | }, 257 | { 258 | desc: "no properties set", 259 | input: StyleProperties{}, 260 | want: "", 261 | }, 262 | { 263 | desc: "sanitize comment in regular value", 264 | input: StyleProperties{ 265 | BackgroundRepeat: "// This is bad", 266 | BackgroundPosition: "/* This is bad", 267 | BackgroundSize: "This is bad */", 268 | }, 269 | want: "background-position:zGoSafezInvalidPropertyValue;" + 270 | `background-repeat:zGoSafezInvalidPropertyValue;` + 271 | `background-size:zGoSafezInvalidPropertyValue;`, 272 | }, 273 | { 274 | desc: "sanitize comment in middle of regular value", 275 | input: StyleProperties{ 276 | BackgroundRepeat: "10px /* This is bad", 277 | BackgroundPosition: "10px // This is bad", 278 | BackgroundSize: "10px */ This is bad", 279 | }, 280 | want: "background-position:zGoSafezInvalidPropertyValue;" + 281 | `background-repeat:zGoSafezInvalidPropertyValue;` + 282 | `background-size:zGoSafezInvalidPropertyValue;`, 283 | }, 284 | { 285 | desc: "sanitize bad rune in regular value", 286 | input: StyleProperties{ 287 | BackgroundSize: "This&is$bad", 288 | }, 289 | want: "background-size:zGoSafezInvalidPropertyValue;", 290 | }, 291 | { 292 | desc: "sanitize invalid enum value", 293 | input: StyleProperties{ 294 | Display: "badValue123", 295 | }, 296 | want: "display:zGoSafezInvalidPropertyValue;", 297 | }, 298 | { 299 | desc: "sanitize unsafe URL value", 300 | input: StyleProperties{ 301 | BackgroundImageURLs: []string{"javascript:badJavascript();"}, 302 | }, 303 | want: `background-image:url("about:invalid#zGoSafez");`, 304 | }, 305 | { 306 | desc: "sanitize regular and enum properties with newline prefix", 307 | input: StyleProperties{ 308 | Display: "\nfoo", 309 | BackgroundColor: "\nfoo", 310 | }, 311 | want: "display:zGoSafezInvalidPropertyValue;background-color:zGoSafezInvalidPropertyValue;", 312 | }, 313 | { 314 | desc: "sanitize regular and enum properties with newline suffix", 315 | input: StyleProperties{ 316 | Display: "foo\n", 317 | BackgroundColor: "foo\n", 318 | }, 319 | want: "display:zGoSafezInvalidPropertyValue;background-color:zGoSafezInvalidPropertyValue;", 320 | }, 321 | { 322 | desc: "regular value symbols in value", 323 | input: StyleProperties{ 324 | BackgroundSize: "*+/-.!#%_ \t", 325 | }, 326 | want: "background-size:*+/-.!#%_ \t;", 327 | }, 328 | { 329 | desc: "quoted and unquoted font family names CSS-escaped", 330 | input: StyleProperties{ 331 | FontFamily: []string{ 332 | `"`, 333 | `""`, 334 | `serif\`, 335 | `"Gulim\Che"`, 336 | `"Gulim"Che"`, 337 | `New Century Schoolbook"`, 338 | `"New Century Schoolbook`, 339 | `New Century" Schoolbook`, 340 | `sans-"serif`, 341 | }, 342 | }, 343 | want: `font-family:"\000022", ` + 344 | `"\000022\000022", ` + 345 | `"serif\00005C", ` + 346 | `"Gulim\00005CChe", ` + 347 | `"Gulim\000022Che", ` + 348 | `"New Century Schoolbook\000022", ` + 349 | `"\000022New Century Schoolbook", ` + 350 | `"New Century\000022 Schoolbook", ` + 351 | `"sans-\000022serif";`, 352 | }, 353 | { 354 | desc: "less-than rune CSS-escaped", 355 | input: StyleProperties{ 356 | BackgroundImageURLs: []string{``}, 357 | FontFamily: []string{``}, 358 | }, 359 | want: `background-image:url("\00003C/style>\00003Cscript>evil()\00003C/script>");` + 360 | `font-family:"\00003C/style>\00003Cscript>evil()\00003C/script>";`, 361 | }, 362 | } { 363 | got := StyleFromProperties(test.input).String() 364 | if got != test.want { 365 | t.Errorf("%s:\ngot:\n\t%s\nwant\n\t%s", test.desc, got, test.want) 366 | } 367 | } 368 | } 369 | 370 | // TestStyleFromPropertiesAllFieldsValidated will fail if any fields in 371 | // StyleProperties are not safely validated by StyleFromProperties. 372 | // 373 | // This is a sanity check to make sure that all fields are validated and tested. 374 | // If a new field is added but not validated, this test will most likely fail. 375 | func TestStyleFromPropertiesAllFieldsValidated(t *testing.T) { 376 | // Use reflection to set all fields in StyleProperties. 377 | var style StyleProperties 378 | v := reflect.ValueOf(&style).Elem() 379 | const badValue = `` 380 | for i := 0; i < v.NumField(); i++ { 381 | f := v.Field(i) 382 | switch f.Type().Kind() { 383 | case reflect.String: 384 | f.SetString(badValue) 385 | case reflect.Slice: 386 | if f.Type().Elem().Kind() == reflect.String { 387 | f.Set(reflect.ValueOf([]string{badValue})) 388 | } else { 389 | t.Fatalf("unknown slice type for field %q in StyleProperties", v.Type().Field(i).Name) 390 | } 391 | default: 392 | t.Fatalf("unknown %s field %q in StyleProperties", f.Type().Kind(), v.Type().Field(i).Name) 393 | } 394 | } 395 | const want = `background-image:url("\00003C/style>\00003Cscript>evil()\00003C/script>");` + 396 | `font-family:"\00003C/style>\00003Cscript>evil()\00003C/script>";` + 397 | `display:zGoSafezInvalidPropertyValue;` + 398 | `background-color:zGoSafezInvalidPropertyValue;` + 399 | `background-position:zGoSafezInvalidPropertyValue;` + 400 | `background-repeat:zGoSafezInvalidPropertyValue;` + 401 | `background-size:zGoSafezInvalidPropertyValue;` + 402 | `color:zGoSafezInvalidPropertyValue;` + 403 | `height:zGoSafezInvalidPropertyValue;` + 404 | `width:zGoSafezInvalidPropertyValue;` + 405 | `left:zGoSafezInvalidPropertyValue;` + 406 | `right:zGoSafezInvalidPropertyValue;` + 407 | `top:zGoSafezInvalidPropertyValue;` + 408 | `bottom:zGoSafezInvalidPropertyValue;` + 409 | `font-weight:zGoSafezInvalidPropertyValue;` + 410 | `padding:zGoSafezInvalidPropertyValue;` + 411 | `z-index:zGoSafezInvalidPropertyValue;` 412 | got := StyleFromProperties(style).String() 413 | if got != want { 414 | t.Errorf("got:\n\t%s\nwant\n\t%s", got, want) 415 | } 416 | } 417 | 418 | func TestCSSEscapeString(t *testing.T) { 419 | for _, test := range [...]struct { 420 | desc, input, output string 421 | }{ 422 | { 423 | desc: "escape disallowed codepoints in ", 424 | input: "\"\\\n", 425 | output: `\000022\00005C\00000A`, 426 | }, 427 | { 428 | desc: "escape control characters", 429 | input: "\u0001\u001F\u007F\u0080\u0090\u009F\u2028\u2029", 430 | output: `\000001\00001F\00007F\000080\000090\00009F\002028\002029`, 431 | }, 432 | { 433 | desc: "escape '<'", 434 | input: "<", 435 | output: `\00003C`, 436 | }, 437 | { 438 | desc: "substitute NULL", 439 | input: "\u0000", 440 | output: "\uFFFD", 441 | }, 442 | { 443 | desc: "no escaping required", 444 | input: `this(can_BE$s4fely:Quoted`, 445 | output: `this(can_BE$s4fely:Quoted`, 446 | }, 447 | } { 448 | escaped := cssEscapeString(test.input) 449 | if escaped != test.output { 450 | t.Errorf("%s:\ngot:\n\t%s\nwant\n\t%s", test.desc, escaped, test.output) 451 | } 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /stylesheet.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "container/list" 11 | "fmt" 12 | "regexp" 13 | "strings" 14 | ) 15 | 16 | // A StyleSheet is an immutable string-like type which represents a CSS 17 | // style sheet and guarantees that its value, as a string, will not cause 18 | // untrusted script execution (cross-site scripting) when evaluated as CSS 19 | // in a browser. 20 | // 21 | // StyleSheet's string representation can safely be interpolated as the 22 | // content of a style element within HTML. The StyleSheet string should 23 | // not be escaped before interpolation. 24 | type StyleSheet struct { 25 | // We declare a StyleSheet not as a string but as a struct wrapping a string 26 | // to prevent construction of StyleSheet values through string conversion. 27 | str string 28 | } 29 | 30 | // StyleSheetFromConstant constructs a StyleSheet with the 31 | // underlying stylesheet set to the given styleSheet, which must be an untyped string 32 | // constant. 33 | // 34 | // No runtime validation or sanitization is performed on script; being under 35 | // application control, it is simply assumed to comply with the StyleSheet 36 | // contract. 37 | func StyleSheetFromConstant(styleSheet stringConstant) StyleSheet { 38 | return StyleSheet{string(styleSheet)} 39 | } 40 | 41 | // CSSRule constructs a StyleSheet containng a CSS rule of the form: 42 | // 43 | // selector{style} 44 | // 45 | // It returns an error if selector contains disallowed characters or unbalanced 46 | // brackets. 47 | // 48 | // The constructed StyleSheet value is guaranteed to fulfill its type contract, 49 | // but is not guaranteed to be semantically valid CSS. 50 | func CSSRule(selector string, style Style) (StyleSheet, error) { 51 | if strings.ContainsRune(selector, '<') { 52 | return StyleSheet{}, fmt.Errorf("selector %q contains '<'", selector) 53 | } 54 | selectorWithoutStrings := cssStringPattern.ReplaceAllString(selector, "") 55 | if matches := invalidCSSSelectorRune.FindStringSubmatch(selectorWithoutStrings); matches != nil { 56 | return StyleSheet{}, fmt.Errorf("selector %q contains %q, which is disallowed outside of CSS strings", selector, matches[0]) 57 | } 58 | if !hasBalancedBrackets(selectorWithoutStrings) { 59 | return StyleSheet{}, fmt.Errorf("selector %q contains unbalanced () or [] brackets", selector) 60 | } 61 | return StyleSheet{fmt.Sprintf("%s{%s}", selector, style.String())}, nil 62 | } 63 | 64 | var ( 65 | // cssStringPattern matches a single- or double-quoted CSS string. 66 | cssStringPattern = regexp.MustCompile( 67 | `"([^"\r\n\f\\]|\\[\s\S])*"|` + // Double-quoted string literal 68 | `'([^'\r\n\f\\]|\\[\s\S])*'`) // Single-quoted string literal 69 | 70 | // invalidCSSSelectorRune matches a rune that is not allowed in a CSS3 71 | // selector that does not contain string literals. 72 | // See https://w3.org/TR/css3-selectors/#selectors. 73 | invalidCSSSelectorRune = regexp.MustCompile(`[^-_a-zA-Z0-9#.:* ,>+~[\]()=^$|]`) 74 | ) 75 | 76 | // hasBalancedBrackets returns whether s has balanced () and [] brackets. 77 | func hasBalancedBrackets(s string) bool { 78 | stack := list.New() 79 | for i := 0; i < len(s); i++ { 80 | c := s[i] 81 | if expected, ok := matchingBrackets[c]; ok { 82 | e := stack.Back() 83 | if e == nil { 84 | return false 85 | } 86 | // Skip success check for this type assertion since it is trivial to 87 | // see that only bytes are pushed onto this stack. 88 | if v := e.Value.(byte); v != expected { 89 | return false 90 | } 91 | stack.Remove(e) 92 | continue 93 | } 94 | for _, openBracket := range matchingBrackets { 95 | if c == openBracket { 96 | stack.PushBack(c) 97 | break 98 | } 99 | } 100 | } 101 | return stack.Len() == 0 102 | } 103 | 104 | // matchingBrackets[x] is the opening bracket that matches closing bracket x. 105 | var matchingBrackets = map[byte]byte{ 106 | ')': '(', 107 | ']': '[', 108 | } 109 | 110 | // String returns the string form of the StyleSheet. 111 | func (s StyleSheet) String() string { 112 | return s.str 113 | } 114 | -------------------------------------------------------------------------------- /stylesheet_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "fmt" 11 | "testing" 12 | ) 13 | 14 | func TestCSSRule(t *testing.T) { 15 | for _, test := range [...]struct { 16 | selector string 17 | style Style 18 | want, err string 19 | }{ 20 | { 21 | `#id`, StyleFromConstant(`top:0;left:0;`), 22 | `#id{top:0;left:0;}`, ``, 23 | }, 24 | { 25 | `.class`, StyleFromConstant(`margin-left:5px;`), 26 | `.class{margin-left:5px;}`, ``, 27 | }, 28 | { 29 | `tag #id, .class`, StyleFromConstant(`color:black !important;`), 30 | `tag #id, .class{color:black !important;}`, ``, 31 | }, 32 | { 33 | `[title='son\'s']`, Style{}, 34 | `[title='son\'s']{}`, ``, 35 | }, 36 | { 37 | `[title="{"]`, Style{}, 38 | `[title="{"]{}`, ``, 39 | }, 40 | { 41 | `:nth-child(1)`, Style{}, 42 | `:nth-child(1){}`, ``, 43 | }, 44 | { 45 | `tag{color:black;}`, Style{}, 46 | ``, `selector "tag{color:black;}" contains "{", which is disallowed outside of CSS strings`, 47 | }, 48 | { 49 | `]`, Style{}, 50 | ``, `selector "]" contains unbalanced () or [] brackets`, 51 | }, 52 | { 53 | `[title`, Style{}, 54 | ``, `selector "[title" contains unbalanced () or [] brackets`, 55 | }, 56 | { 57 | `[foo)bar]`, Style{}, 58 | ``, `selector "[foo)bar]" contains unbalanced () or [] brackets`, 59 | }, 60 | { 61 | `[foo[bar]`, Style{}, 62 | ``, `selector "[foo[bar]" contains unbalanced () or [] brackets`, 63 | }, 64 | { 65 | `foo(bar(baz)`, Style{}, 66 | ``, `selector "foo(bar(baz)" contains unbalanced () or [] brackets`, 67 | }, 68 | { 69 | `:nth-child(1`, Style{}, 70 | ``, `selector ":nth-child(1" contains unbalanced () or [] brackets`, 71 | }, 72 | { 73 | `[type="a]`, Style{}, 74 | ``, `selector "[type=\"a]" contains "\"", which is disallowed outside of CSS strings`, 75 | }, 76 | { 77 | `[type=\'a]`, Style{}, 78 | ``, `selector "[type=\\'a]" contains "\\", which is disallowed outside of CSS strings`, 79 | }, 80 | { 81 | `<`, Style{}, 82 | ``, `selector "<" contains '<'`, 83 | }, 84 | { 85 | `@import "foo";#id`, Style{}, 86 | ``, `selector "@import \"foo\";#id" contains "@", which is disallowed outside of CSS strings`, 87 | }, 88 | { 89 | `/* `, Style{}, 90 | ``, `selector "/* " contains "/", which is disallowed outside of CSS strings`, 91 | }, 92 | } { 93 | errPrefix := fmt.Sprintf("CSSRule(%q, %#v)", test.selector, test.style) 94 | ss, err := CSSRule(test.selector, test.style) 95 | if test.want != "" && err != nil { 96 | t.Errorf("%s returned unexpected error: %s", errPrefix, err) 97 | } else if test.want != "" && ss.String() != test.want { 98 | t.Errorf("%s = %q, want: %q", errPrefix, ss.String(), test.want) 99 | } else if test.want == "" && err == nil { 100 | t.Errorf("%s expected error", errPrefix) 101 | } else if test.want == "" && err.Error() != test.err { 102 | t.Errorf("%s returned error:\n\t%s,\nwant:\n\t%q", errPrefix, err, test.want) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /template/clone_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "strings" 13 | "sync" 14 | "testing" 15 | ) 16 | 17 | func TestClone(t *testing.T) { 18 | // The {{.}} will be executed with data `*/` in different contexts. 19 | // In the t0 template, it will be in a HTML context. 20 | // In the t1 template, it will be in a URL query context. 21 | // In the t2 template, it will be in a Script context. 22 | // In the t3 template, it will be in a StyleSheet context. 23 | const tmpl = `{{define "a"}}{{template "lhs"}}{{.}}{{template "rhs"}}{{end}}` 24 | const data = `*/` 25 | b := new(bytes.Buffer) 26 | 27 | // Create an incomplete template t0. 28 | t0 := Must(New("t0").Parse(tmpl)) 29 | 30 | // Clone t0 as t1. 31 | t1 := Must(t0.Clone()) 32 | Must(t1.Parse(`{{define "lhs"}} {{end}}`)) 34 | 35 | // Execute t1. 36 | b.Reset() 37 | if err := t1.ExecuteTemplate(b, "a", data); err != nil { 38 | t.Fatal(err) 39 | } 40 | if got, want := b.String(), ` `; got != want { 41 | t.Errorf("t1: got %q want %q", got, want) 42 | } 43 | 44 | // Clone t0 as t2. 45 | t2 := Must(t0.Clone()) 46 | Must(t2.Parse(`{{define "lhs"}} {{end}}`)) 48 | 49 | // Execute t2. 50 | b.Reset() 51 | err := t2.ExecuteTemplate(b, "a", data) 52 | if err == nil { 53 | t.Fatalf("t2: got %q, expected error", b.String()) 54 | } 55 | if got, want := err.Error(), `expected a safehtml.Script value`; !strings.Contains(got, want) { 56 | t.Errorf("t2: error\n\t%q\ndoes not contain\n\t%q", got, want) 57 | } 58 | 59 | // Clone t0 as t3, but do not execute t3 yet. 60 | t3 := Must(t0.Clone()) 61 | Must(t3.Parse(`{{define "lhs"}} {{end}}`)) 63 | 64 | // Complete t0. 65 | Must(t0.Parse(`{{define "lhs"}} ( {{end}}`)) 66 | Must(t0.Parse(`{{define "rhs"}} ) {{end}}`)) 67 | 68 | // Clone t0 as t4. Redefining the "lhs" template should not fail. 69 | t4 := Must(t0.Clone()) 70 | if _, err := t4.Parse(`{{define "lhs"}} OK {{end}}`); err != nil { 71 | t.Errorf(`redefine "lhs": got err %v want nil`, err) 72 | } 73 | // Cloning t1 should fail as it has been executed. 74 | if _, err := t1.Clone(); err == nil { 75 | t.Error("cloning t1: got nil err want non-nil") 76 | } 77 | // Redefining the "lhs" template in t1 should fail as it has been executed. 78 | if _, err := t1.Parse(`{{define "lhs"}} OK {{end}}`); err == nil { 79 | t.Error(`redefine "lhs": got nil err want non-nil`) 80 | } 81 | 82 | // Execute t0. 83 | b.Reset() 84 | if err := t0.ExecuteTemplate(b, "a", data); err != nil { 85 | t.Fatal(err) 86 | } 87 | if got, want := b.String(), ` ( <i>*/ ) `; got != want { 88 | t.Errorf("t0: got %q want %q", got, want) 89 | } 90 | 91 | // Clone t0. This should fail, as t0 has already executed. 92 | if _, err := t0.Clone(); err == nil { 93 | t.Error(`t0.Clone(): got nil err want non-nil`) 94 | } 95 | 96 | // Similarly, cloning sub-templates should fail. 97 | if _, err := t0.Lookup("a").Clone(); err == nil { 98 | t.Error(`t0.Lookup("a").Clone(): got nil err want non-nil`) 99 | } 100 | if _, err := t0.Lookup("lhs").Clone(); err == nil { 101 | t.Error(`t0.Lookup("lhs").Clone(): got nil err want non-nil`) 102 | } 103 | 104 | // Execute t3. 105 | b.Reset() 106 | err = t3.ExecuteTemplate(b, "a", data) 107 | if err == nil { 108 | t.Fatalf("t3: got %q, expected error", b.String()) 109 | } 110 | if got, want := err.Error(), `expected a safehtml.StyleSheet value`; !strings.Contains(got, want) { 111 | t.Errorf("t3: error\n\t%q\ndoes not contain\n\t%q", got, want) 112 | } 113 | } 114 | 115 | // This used to crash; https://golang.org/issue/3281 116 | func TestCloneCrash(t *testing.T) { 117 | t1 := New("all") 118 | Must(t1.New("t1").Parse(`{{define "foo"}}foo{{end}}`)) 119 | t1.Clone() 120 | } 121 | 122 | // Ensure that this guarantee from the docs is upheld: 123 | // "Further calls to Parse in the copy will add templates 124 | // to the copy but not to the original." 125 | func TestCloneThenParse(t *testing.T) { 126 | t0 := Must(New("t0").Parse(`{{define "a"}}{{template "embedded"}}{{end}}`)) 127 | t1 := Must(t0.Clone()) 128 | Must(t1.Parse(`{{define "embedded"}}t1{{end}}`)) 129 | if len(t0.Templates())+1 != len(t1.Templates()) { 130 | t.Error("adding a template to a clone added it to the original") 131 | } 132 | // double check that the embedded template isn't available in the original 133 | err := t0.ExecuteTemplate(ioutil.Discard, "a", nil) 134 | if err == nil { 135 | t.Error("expected 'no such template' error") 136 | } 137 | } 138 | 139 | // https://golang.org/issue/5980 140 | func TestFuncMapWorksAfterClone(t *testing.T) { 141 | funcs := FuncMap{"customFunc": func() (string, error) { 142 | return "", errors.New("issue5980") 143 | }} 144 | 145 | // get the expected error output (no clone) 146 | uncloned := Must(New("").Funcs(funcs).Parse("{{customFunc}}")) 147 | wantErr := uncloned.Execute(ioutil.Discard, nil) 148 | 149 | // toClone must be the same as uncloned. It has to be recreated from scratch, 150 | // since cloning cannot occur after execution. 151 | toClone := Must(New("").Funcs(funcs).Parse("{{customFunc}}")) 152 | cloned := Must(toClone.Clone()) 153 | gotErr := cloned.Execute(ioutil.Discard, nil) 154 | 155 | if wantErr.Error() != gotErr.Error() { 156 | t.Errorf("clone error message mismatch want %q got %q", wantErr, gotErr) 157 | } 158 | } 159 | 160 | // https://golang.org/issue/16101 161 | func TestTemplateCloneExecuteRace(t *testing.T) { 162 | const ( 163 | input = `{{block "a" .}}a{{end}}{{block "b" .}}b{{end}}` 164 | overlay = `{{define "b"}}A{{end}}` 165 | ) 166 | outer := Must(New("outer").Parse(input)) 167 | tmpl := Must(Must(outer.Clone()).Parse(overlay)) 168 | 169 | var wg sync.WaitGroup 170 | for i := 0; i < 10; i++ { 171 | wg.Add(1) 172 | go func() { 173 | defer wg.Done() 174 | for i := 0; i < 100; i++ { 175 | if err := tmpl.Execute(ioutil.Discard, "data"); err != nil { 176 | panic(err) 177 | } 178 | } 179 | }() 180 | } 181 | wg.Wait() 182 | } 183 | 184 | func TestTemplateCloneLookup(t *testing.T) { 185 | // Template.escape makes an assumption that the template associated 186 | // with t.Name() is t. Check that this holds. 187 | tmpl := Must(New("x").Parse("a")) 188 | tmpl = Must(tmpl.Clone()) 189 | if tmpl.Lookup(tmpl.Name()) != tmpl { 190 | t.Error("after Clone, tmpl.Lookup(tmpl.Name()) != tmpl") 191 | } 192 | } 193 | 194 | func TestCloneGrowth(t *testing.T) { 195 | tmpl := Must(New("root").Parse(`{{block "B". }}Arg{{end}}`)) 196 | tmpl = Must(tmpl.Clone()) 197 | Must(tmpl.Parse(`{{define "B"}}Text{{end}}`)) 198 | for i := 0; i < 10; i++ { 199 | tmpl.Execute(ioutil.Discard, nil) 200 | } 201 | if len(tmpl.DefinedTemplates()) > 200 { 202 | t.Fatalf("too many templates: %v", len(tmpl.DefinedTemplates())) 203 | } 204 | } 205 | 206 | // https://golang.org/issue/17735 207 | func TestCloneRedefinedName(t *testing.T) { 208 | const base = ` 209 | {{ define "a" -}}{{ template "b" . -}}{{ end -}} 210 | {{ define "b" }}{{ end -}} 211 | ` 212 | const page = `{{ template "a" . }}` 213 | 214 | t1 := Must(New("a").Parse(base)) 215 | 216 | for i := 0; i < 2; i++ { 217 | t2 := Must(t1.Clone()) 218 | t2 = Must(t2.New(fmt.Sprintf("%d", i)).Parse(page)) 219 | err := t2.Execute(ioutil.Discard, nil) 220 | if err != nil { 221 | t.Fatal(err) 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /template/content_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "testing" 11 | ) 12 | 13 | // Test that we print using the String method. Was issue 3073. 14 | type stringer struct { 15 | v int 16 | } 17 | 18 | func (s *stringer) String() string { 19 | return fmt.Sprintf("string=%d", s.v) 20 | } 21 | 22 | type errorer struct { 23 | v int 24 | } 25 | 26 | func (s *errorer) Error() string { 27 | return fmt.Sprintf("error=%d", s.v) 28 | } 29 | 30 | func TestStringer(t *testing.T) { 31 | s := &stringer{3} 32 | b := new(bytes.Buffer) 33 | tmpl := Must(New("x").Parse("{{.}}")) 34 | if err := tmpl.Execute(b, s); err != nil { 35 | t.Fatal(err) 36 | } 37 | var expect = "string=3" 38 | if b.String() != expect { 39 | t.Errorf("expected %q got %q", expect, b.String()) 40 | } 41 | e := &errorer{7} 42 | b.Reset() 43 | if err := tmpl.Execute(b, e); err != nil { 44 | t.Fatal(err) 45 | } 46 | expect = "error=7" 47 | if b.String() != expect { 48 | t.Errorf("expected %q got %q", expect, b.String()) 49 | } 50 | } 51 | 52 | // https://golang.org/issue/5982 53 | func TestEscapingNilNonemptyInterfaces(t *testing.T) { 54 | tmpl := Must(New("x").Parse("{{.E}}")) 55 | 56 | got := new(bytes.Buffer) 57 | testData := struct{ E error }{} // any non-empty interface here will do; error is just ready at hand 58 | tmpl.Execute(got, testData) 59 | 60 | // Use this data instead of just hard-coding "<nil>" to avoid 61 | // dependencies on the html escaper and the behavior of fmt w.r.t. nil. 62 | want := new(bytes.Buffer) 63 | data := struct{ E string }{E: fmt.Sprint(nil)} 64 | tmpl.Execute(want, data) 65 | 66 | if !bytes.Equal(want.Bytes(), got.Bytes()) { 67 | t.Errorf("expected %q got %q", string(want.Bytes()), string(got.Bytes())) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /template/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "strings" 9 | ) 10 | 11 | // context describes the state an HTML parser must be in when it reaches the 12 | // portion of HTML produced by evaluating a particular template node. 13 | // 14 | // The zero value of type Context is the start context for a template that 15 | // produces an HTML fragment as defined at 16 | // http://www.w3.org/TR/html5/syntax.html#the-end 17 | // where the context element is null. 18 | type context struct { 19 | state state 20 | delim delim 21 | element element 22 | attr attr 23 | err *Error 24 | // scriptType is the lowercase value of the "type" attribute inside the current "script" 25 | // element (see https://dev.w3.org/html5/spec-preview/the-script-element.html#attr-script-type). 26 | // This field will be empty if the parser is currently not in a script element, 27 | // the type attribute has not already been parsed in the current element, or if the 28 | // value of the type attribute cannot be determined at parse time. 29 | scriptType string 30 | // linkRel is the value of the "rel" attribute inside the current "link" 31 | // element (see https://html.spec.whatwg.org/multipage/semantics.html#attr-link-rel). 32 | // This value has been normalized to lowercase with exactly one space between tokens 33 | // and exactly one space at start and end, so that a lookup of any token foo can 34 | // be performed by searching for the substring " foo ". 35 | // This field will be empty if the parser is currently not in a link element, 36 | // the rel attribute has not already been parsed in the current element, or if the 37 | // value of the rel attribute cannot be determined at parse time. 38 | linkRel string 39 | } 40 | 41 | // eq returns whether Context c is equal to Context d. 42 | func (c context) eq(d context) bool { 43 | return c.state == d.state && 44 | c.delim == d.delim && 45 | c.element.eq(d.element) && 46 | c.attr.eq(d.attr) && 47 | c.err == d.err && 48 | c.scriptType == d.scriptType && 49 | c.linkRel == d.linkRel 50 | } 51 | 52 | // state describes a high-level HTML parser state. 53 | // 54 | // It bounds the top of the element stack, and by extension the HTML insertion 55 | // mode, but also contains state that does not correspond to anything in the 56 | // HTML5 parsing algorithm because a single token production in the HTML 57 | // grammar may contain embedded actions in a template. For instance, the quoted 58 | // HTML attribute produced by 59 | // 60 | //
61 | // 62 | // is a single token in HTML's grammar but in a template spans several nodes. 63 | type state uint8 64 | 65 | //go:generate stringer -type state 66 | 67 | const ( 68 | // stateText is parsed character data. An HTML parser is in 69 | // this state when its parse position is outside an HTML tag, 70 | // directive, comment, and special element body. 71 | stateText state = iota 72 | // stateSpecialElementBody occurs inside a specal HTML element body. 73 | stateSpecialElementBody 74 | // stateTag occurs before an HTML attribute or the end of a tag. 75 | stateTag 76 | // stateAttrName occurs inside an attribute name. 77 | // It occurs between the ^'s in ` ^name^ = value`. 78 | stateAttrName 79 | // stateAfterName occurs after an attr name has ended but before any 80 | // equals sign. It occurs between the ^'s in ` name^ ^= value`. 81 | stateAfterName 82 | // stateBeforeValue occurs after the equals sign but before the value. 83 | // It occurs between the ^'s in ` name =^ ^value`. 84 | stateBeforeValue 85 | // stateHTMLCmt occurs inside an . 86 | stateHTMLCmt 87 | // stateAttr occurs inside an HTML attribute whose content is text. 88 | stateAttr 89 | // stateError is an infectious error state outside any valid 90 | // HTML/CSS/JS construct. 91 | stateError 92 | ) 93 | 94 | // isComment reports whether a state contains content meant for template 95 | // authors & maintainers, not for end-users or machines. 96 | func isComment(s state) bool { 97 | switch s { 98 | case stateHTMLCmt: 99 | return true 100 | } 101 | return false 102 | } 103 | 104 | // isInTag reports whether s occurs solely inside an HTML tag. 105 | func isInTag(s state) bool { 106 | switch s { 107 | case stateTag, stateAttrName, stateAfterName, stateBeforeValue, stateAttr: 108 | return true 109 | } 110 | return false 111 | } 112 | 113 | // delim is the delimiter that will end the current HTML attribute. 114 | type delim uint8 115 | 116 | //go:generate stringer -type delim 117 | 118 | const ( 119 | // delimNone occurs outside any attribute. 120 | delimNone delim = iota 121 | // delimDoubleQuote occurs when a double quote (") closes the attribute. 122 | delimDoubleQuote 123 | // delimSingleQuote occurs when a single quote (') closes the attribute. 124 | delimSingleQuote 125 | // delimSpaceOrTagEnd occurs when a space or right angle bracket (>) 126 | // closes the attribute. 127 | delimSpaceOrTagEnd 128 | ) 129 | 130 | type element struct { 131 | // name is the lowercase name of the element. If context joining has occurred, name 132 | // will be arbitrarily assigned the element name from one of the joined contexts. 133 | name string 134 | // names contains all possible names the element could assume because of context joining. 135 | // For example, after joining the contexts in the "if" and "else" branches of 136 | // {{if .C}}`, 137 | // names will contain "img" and "audio". 138 | // names can also contain empty strings, which represent joined contexts with no element name. 139 | // names will be empty if no context joining occurred. 140 | names []string 141 | } 142 | 143 | // eq reports whether a and b have the same name. All other fields are ignored. 144 | func (e element) eq(d element) bool { 145 | return e.name == d.name 146 | } 147 | 148 | // String returns the string representation of the element. 149 | func (e element) String() string { 150 | return "element" + strings.Title(e.name) 151 | } 152 | 153 | // attr represents the attribute that the parser is in, that is, 154 | // starting from stateAttrName until stateTag/stateText (exclusive). 155 | type attr struct { 156 | // name is the lowercase name of the attribute. If context joining has occurred, name 157 | // will be arbitrarily assigned the attribute name from one of the joined contexts. 158 | name string 159 | // value is the value of the attribute. If context joining has occurred, value 160 | // will be arbitrarily assigned the attribute value from one of the joined contexts. 161 | // If there are multiple actions in the attribute value, value will contain the 162 | // concatenation of all values seen so far. For example, in 163 | // 164 | // value is "foo" at "{{.X}}" and "foobar" at "{{.Y}}". 165 | value string 166 | // ambiguousValue indicates whether value contains an ambiguous value due to context-joining. 167 | ambiguousValue bool 168 | // names contains all possible names the attribute could assume because of context joining. 169 | // For example, after joining the contexts in the "if" and "else" branches of 170 | // 171 | // names will contain "title" and "name". 172 | // names can also contain empty strings, which represent joined contexts with no attribute name. 173 | // names will be empty if no context joining occurred. 174 | names []string 175 | } 176 | 177 | // eq reports whether a and b have the same name. All other fields are ignored. 178 | func (a attr) eq(b attr) bool { 179 | return a.name == b.name 180 | } 181 | 182 | // String returns the string representation of the attr. 183 | func (a attr) String() string { 184 | return "attr" + strings.Title(a.name) 185 | } 186 | -------------------------------------------------------------------------------- /template/delim_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type Delim"; DO NOT EDIT 2 | 3 | package template 4 | 5 | import "fmt" 6 | 7 | const _Delim_name = "DelimNoneDelimDoubleQuoteDelimSingleQuoteDelimSpaceOrTagEnd" 8 | 9 | var _Delim_index = [...]uint8{0, 9, 25, 41, 59} 10 | 11 | func (i delim) String() string { 12 | if i >= delim(len(_Delim_index)-1) { 13 | return fmt.Sprintf("delim(%d)", i) 14 | } 15 | return _Delim_name[_Delim_index[i]:_Delim_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /template/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "fmt" 9 | "text/template/parse" 10 | ) 11 | 12 | // Error describes a problem encountered during template Escaping. 13 | type Error struct { 14 | // ErrorCode describes the kind of error. 15 | ErrorCode ErrorCode 16 | // Node is the node that caused the problem, if known. 17 | // If not nil, it overrides Name and Line. 18 | Node parse.Node 19 | // Name is the name of the template in which the error was encountered. 20 | Name string 21 | // Line is the line number of the error in the template source or 0. 22 | Line int 23 | // Description is a human-readable description of the problem. 24 | Description string 25 | } 26 | 27 | // ErrorCode is a code for a kind of error. 28 | type ErrorCode int 29 | 30 | // We define codes for each error that manifests while escaping templates, but 31 | // escaped templates may also fail at runtime. 32 | // 33 | // Output: "ZgotmplZ" 34 | // Example: 35 | // 36 | // 37 | // where {{.X}} evaluates to `javascript:...` 38 | // 39 | // Discussion: 40 | // 41 | // "ZgotmplZ" is a special value that indicates that unsafe content reached a 42 | // CSS or URL context at runtime. The output of the example will be 43 | // 44 | // If the data comes from a trusted source, use content types to exempt it 45 | // from filtering: URL(`javascript:...`). 46 | const ( 47 | // OK indicates the lack of an error. 48 | OK ErrorCode = iota 49 | 50 | // ErrAmbigContext: "... appears in an ambiguous context within a URL" 51 | // Example: 52 | // 60 | // Discussion: 61 | // {{.X}} is in an ambiguous URL context since, depending on {{.C}}, 62 | // it may be either a URL suffix or a query parameter. 63 | // Moving {{.X}} into the condition removes the ambiguity: 64 | // 65 | ErrAmbigContext 66 | 67 | // ErrBadHTML: "expected space, attr name, or end of tag, but got ...", 68 | // "... in unquoted attr", "... in attribute name" 69 | // Example: 70 | // 71 | // 72 | //
73 | //
{{end}} 120 | // {{define "attrs"}}href="{{.URL}}"{{end}} 121 | // Discussion: 122 | // Package html/template looks through template calls to compute the 123 | // context. 124 | // Here the {{.URL}} in "attrs" must be treated as a URL when called 125 | // from "main", but you will get this error if "attrs" is not defined 126 | // when "main" is parsed. 127 | ErrNoSuchTemplate 128 | 129 | // ErrOutputContext: "cannot compute output context for template ..." 130 | // Examples: 131 | // {{define "t"}}{{if .T}}{{template "t" .T}}{{end}}{{.H}}",{{end}} 132 | // Discussion: 133 | // A recursive template does not end in the same context in which it 134 | // starts, and a reliable output context cannot be computed. 135 | // Look for typos in the named template. 136 | // If the template should not be called in the named start context, 137 | // look for calls to that template in unexpected contexts. 138 | // Maybe refactor recursive templates to not be recursive. 139 | ErrOutputContext 140 | 141 | // ErrPartialCharset: "unfinished JS regexp charset in ..." 142 | // Example: 143 | // 144 | // Discussion: 145 | // Package html/template does not support interpolation into regular 146 | // expression literal character sets. 147 | ErrPartialCharset 148 | 149 | // ErrPartialEscape: "unfinished escape sequence in ..." 150 | // Example: 151 | // 152 | // Discussion: 153 | // Package html/template does not support actions following a 154 | // backslash. 155 | // This is usually an error and there are better solutions; for 156 | // example 157 | // 158 | // should work, and if {{.X}} is a partial escape sequence such as 159 | // "xA0", mark the whole sequence as safe content: JSStr(`\xA0`) 160 | ErrPartialEscape 161 | 162 | // ErrRangeLoopReentry: "on range loop re-entry: ..." 163 | // Example: 164 | // 165 | // Discussion: 166 | // If an iteration through a range would cause it to end in a 167 | // different context than an earlier pass, there is no single context. 168 | // In the example, there is missing a quote, so it is not clear 169 | // whether {{.}} is meant to be inside a JS string or in a JS value 170 | // context. The second iteration would produce something like 171 | // 172 | // 173 | ErrRangeLoopReentry 174 | 175 | // ErrSlashAmbig: '/' could start a division or regexp. 176 | // Example: 177 | // 181 | // Discussion: 182 | // The example above could produce `var x = 1/-2/i.test(s)...` 183 | // in which the first '/' is a mathematical division operator or it 184 | // could produce `/-2/i.test(s)` in which the first '/' starts a 185 | // regexp literal. 186 | // Look for missing semicolons inside branches, and maybe add 187 | // parentheses to make it clear which interpretation you intend. 188 | ErrSlashAmbig 189 | 190 | // ErrPredefinedEscaper: "predefined escaper ... disallowed in template" 191 | // Example: 192 | //
Hello
193 | // Discussion: 194 | // Package html/template already contextually escapes all pipelines to 195 | // produce HTML output safe against code injection. Manually escaping 196 | // pipeline output using the predefined escapers "html" or "urlquery" is 197 | // unnecessary, and may affect the correctness or safety of the escaped 198 | // pipeline output in Go 1.8 and earlier. 199 | // 200 | // In most cases, such as the given example, this error can be resolved by 201 | // simply removing the predefined escaper from the pipeline and letting the 202 | // contextual autoescaper handle the escaping of the pipeline. In other 203 | // instances, where the predefined escaper occurs in the middle of a 204 | // pipeline where subsequent commands expect escaped input, e.g. 205 | // {{.X | html | makeALink}} 206 | // where makeALink does 207 | // return `link` 208 | // consider refactoring the surrounding template to make use of the 209 | // contextual autoescaper, i.e. 210 | // link 211 | // 212 | // To ease migration to Go 1.9 and beyond, "html" and "urlquery" will 213 | // continue to be allowed as the last command in a pipeline. However, if the 214 | // pipeline occurs in an unquoted attribute value context, "html" is 215 | // disallowed. Avoid using "html" and "urlquery" entirely in new templates. 216 | ErrPredefinedEscaper 217 | 218 | // ErrEscapeAction: "cannot escape action ..." 219 | // Discussion: 220 | // Error returned while escaping an action using EscaperForContext. 221 | // Refer to error message for more details. 222 | // TODO: remove this error type and replace it with more informative sanitization errors. 223 | ErrEscapeAction 224 | 225 | // ErrCSPCompatibility: `"javascript:" URI disallowed for CSP compatibility`, 226 | // "inline event handler ... is disallowed for CSP compatibility 227 | // Examples: 228 | // A thing. 229 | // foo 230 | // Discussion: 231 | // Inline event handlers (onclick="...", onerror="...") and 232 | // links can be used to run scripts, 233 | // so an attacker who finds an XSS bug could inject such HTML 234 | // and execute malicious JavaScript. These patterns must be 235 | // refactored into safer alternatives for compatibility with 236 | // Content Security Policy (CSP). 237 | // 238 | // For example, the following HTML that contains an inline event handler: 239 | // 240 | // A thing. 241 | // can be refactored into: 242 | // A thing. 243 | // 249 | // 250 | // Likewise, the following HTML containng a javascript: URI: 251 | // foo 252 | // can be refactored into: 253 | // foo 254 | // 260 | ErrCSPCompatibility 261 | // All JS templates inside script literals have to be balanced; otherwise a concatenation such as 262 | // can contain XSS if data contains user-controlled escaped strings (e.g. as JSON). 263 | ErrUnbalancedJsTemplate 264 | ) 265 | 266 | func (e *Error) Error() string { 267 | switch { 268 | case e.Node != nil: 269 | loc, _ := (*parse.Tree)(nil).ErrorContext(e.Node) 270 | return fmt.Sprintf("html/template:%s: %s", loc, e.Description) 271 | case e.Line != 0: 272 | return fmt.Sprintf("html/template:%s:%d: %s", e.Name, e.Line, e.Description) 273 | case e.Name != "": 274 | return fmt.Sprintf("html/template:%s: %s", e.Name, e.Description) 275 | } 276 | return "html/template: " + e.Description 277 | } 278 | 279 | // errorf creates an error given a format string f and args. 280 | // The template Name still needs to be supplied. 281 | func errorf(k ErrorCode, node parse.Node, line int, f string, args ...interface{}) *Error { 282 | return &Error{k, node, "", line, fmt.Sprintf(f, args...)} 283 | } 284 | -------------------------------------------------------------------------------- /template/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template_test 6 | 7 | import ( 8 | "log" 9 | "os" 10 | "strings" 11 | 12 | "github.com/google/safehtml/template" 13 | ) 14 | 15 | func Example() { 16 | const tpl = ` 17 | 18 | 19 | 20 | 21 | {{.Title}} 22 | 23 | 24 | {{range .Items}}
{{ . }}
{{else}}
no rows
{{end}} 25 | 26 | ` 27 | 28 | check := func(err error) { 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | } 33 | t, err := template.New("webpage").Parse(tpl) 34 | check(err) 35 | 36 | data := struct { 37 | Title string 38 | Items []string 39 | }{ 40 | Title: "My page", 41 | Items: []string{ 42 | "My photos", 43 | "My blog", 44 | }, 45 | } 46 | 47 | err = t.Execute(os.Stdout, data) 48 | check(err) 49 | 50 | noItems := struct { 51 | Title string 52 | Items []string 53 | }{ 54 | Title: "My another page", 55 | Items: []string{}, 56 | } 57 | 58 | err = t.Execute(os.Stdout, noItems) 59 | check(err) 60 | 61 | // Output: 62 | // 63 | // 64 | // 65 | // 66 | // My page 67 | // 68 | // 69 | //
My photos
My blog
70 | // 71 | // 72 | // 73 | // 74 | // 75 | // 76 | // My another page 77 | // 78 | // 79 | //
no rows
80 | // 81 | // 82 | 83 | } 84 | 85 | func Example_autosanitization() { 86 | t := template.Must(template.New("foo").Parse(`{{ .Y }}`)) 87 | renderHTML := func(x, y string) { 88 | if err := t.Execute(os.Stdout, struct{ X, Y string }{x, y}); err != nil { 89 | log.Fatal(err) 90 | } 91 | } 92 | renderHTML("javascript:evil()", "") 93 | // Output: 94 | // </a><script>alert('pwned')</script><a> 95 | } 96 | 97 | // The following example is duplicated in html/template; keep them in sync. 98 | 99 | func ExampleTemplate_block() { 100 | const ( 101 | master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}` 102 | overlay = `{{define "list"}} {{join . ", "}}{{end}} ` 103 | ) 104 | var ( 105 | funcs = template.FuncMap{"join": strings.Join} 106 | guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"} 107 | ) 108 | masterTmpl, err := template.New("master").Funcs(funcs).Parse(master) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay) 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | if err := masterTmpl.Execute(os.Stdout, guardians); err != nil { 117 | log.Fatal(err) 118 | } 119 | if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil { 120 | log.Fatal(err) 121 | } 122 | // Output: 123 | // Names: 124 | // - Gamora 125 | // - Groot 126 | // - Nebula 127 | // - Rocket 128 | // - Star-Lord 129 | // Names: Gamora, Groot, Nebula, Rocket, Star-Lord 130 | } 131 | -------------------------------------------------------------------------------- /template/examplefiles_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template_test 6 | 7 | import ( 8 | "log" 9 | "os" 10 | 11 | "github.com/google/safehtml/template" 12 | ) 13 | 14 | // Here we demonstrate loading a set of templates from a directory. 15 | func ExampleTemplate_glob() { 16 | // Here we load three template files with the following contents: 17 | // testdata/glob_t0.tmpl: `T0 invokes T1: ({{template "T1"}})` 18 | // testdata/glob_t1.tmpl: `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}` 19 | // testdata/glob_t2.tmpl: `{{define "T2"}}This is T2{{end}}` 20 | // Note that ParseGlob only accepts an untyped string constant. 21 | // glob_t0.tmpl is the first name matched, so it becomes the starting template, 22 | // the value returned by ParseGlob. 23 | tmpl := template.Must(template.ParseGlob("testdata/glob_*.tmpl")) 24 | 25 | err := tmpl.Execute(os.Stdout, nil) 26 | if err != nil { 27 | log.Fatalf("template execution: %s", err) 28 | } 29 | // Output: 30 | // T0 invokes T1: (T1 invokes T2: (This is T2)) 31 | } 32 | 33 | // Here we demonstrate loading a set of templates from files in different directories 34 | func ExampleTemplate_parsefiles() { 35 | // Here we load two template files from different directories with the following contents: 36 | // testdata/dir1/parsefiles_t1.tmpl: `T1 invokes T2: ({{template "T2"}})` 37 | // testdata/dir2/parsefiles_t2.tmpl: `{{define "T2"}}This is T2{{end}}` 38 | // Note that ParseFiles only accepts an untyped string constants. 39 | tmpl := template.Must(template.ParseFiles("testdata/dir1/parsefiles_t1.tmpl", "testdata/dir2/parsefiles_t2.tmpl")) 40 | 41 | err := tmpl.Execute(os.Stdout, nil) 42 | if err != nil { 43 | log.Fatalf("template execution: %s", err) 44 | } 45 | // Output: 46 | // T1 invokes T2: (This is T2) 47 | } 48 | 49 | // This example demonstrates one way to share some templates 50 | // and use them in different contexts. In this variant we add multiple driver 51 | // templates by hand to an existing bundle of templates. 52 | func ExampleTemplate_helpers() { 53 | // Here we load the helpers from two template files with the following contents: 54 | // testdata/helpers_t1.tmpl: `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}` 55 | // testdata/helpers_t2.tmpl: `{{define "T2"}}This is T2{{end}}` 56 | // Note that ParseGlob only accepts an untyped string constant. 57 | templates := template.Must(template.ParseGlob("testdata/helpers_*.tmpl")) 58 | // Add one driver template to the bunch; we do this with an explicit template definition. 59 | _, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}") 60 | if err != nil { 61 | log.Fatal("parsing driver1: ", err) 62 | } 63 | // Add another driver template. 64 | _, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}") 65 | if err != nil { 66 | log.Fatal("parsing driver2: ", err) 67 | } 68 | // We load all the templates before execution. This package does not require 69 | // that behavior but html/template's escaping does, so it's a good habit. 70 | err = templates.ExecuteTemplate(os.Stdout, "driver1", nil) 71 | if err != nil { 72 | log.Fatalf("driver1 execution: %s", err) 73 | } 74 | err = templates.ExecuteTemplate(os.Stdout, "driver2", nil) 75 | if err != nil { 76 | log.Fatalf("driver2 execution: %s", err) 77 | } 78 | // Output: 79 | // Driver 1 calls T1: (T1 invokes T2: (This is T2)) 80 | // Driver 2 calls T2: (This is T2) 81 | } 82 | 83 | // This example demonstrates how to use one group of driver 84 | // templates with distinct sets of helper templates. 85 | func ExampleTemplate_share() { 86 | // Here we load the helpers from two template files with the following contents: 87 | // testdata/share_t0.tmpl: "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n" 88 | // testdata/share_t1.tmpl: `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}` 89 | // Note that ParseGlob only accepts an untyped string constant. 90 | drivers := template.Must(template.ParseGlob("testdata/share_*.tmpl")) 91 | 92 | // We must define an implementation of the T2 template. First we clone 93 | // the drivers, then add a definition of T2 to the template name space. 94 | 95 | // 1. Clone the helper set to create a new name space from which to run them. 96 | first, err := drivers.Clone() 97 | if err != nil { 98 | log.Fatal("cloning helpers: ", err) 99 | } 100 | // 2. Define T2, version A, and parse it. 101 | _, err = first.Parse("{{define `T2`}}T2, version A{{end}}") 102 | if err != nil { 103 | log.Fatal("parsing T2: ", err) 104 | } 105 | 106 | // Now repeat the whole thing, using a different version of T2. 107 | // 1. Clone the drivers. 108 | second, err := drivers.Clone() 109 | if err != nil { 110 | log.Fatal("cloning drivers: ", err) 111 | } 112 | // 2. Define T2, version B, and parse it. 113 | _, err = second.Parse("{{define `T2`}}T2, version B{{end}}") 114 | if err != nil { 115 | log.Fatal("parsing T2: ", err) 116 | } 117 | 118 | // Execute the templates in the reverse order to verify the 119 | // first is unaffected by the second. 120 | err = second.ExecuteTemplate(os.Stdout, "share_t0.tmpl", "second") 121 | if err != nil { 122 | log.Fatalf("second execution: %s", err) 123 | } 124 | err = first.ExecuteTemplate(os.Stdout, "share_t0.tmpl", "first") 125 | if err != nil { 126 | log.Fatalf("first: execution: %s", err) 127 | } 128 | 129 | // Output: 130 | // T0 (second version) invokes T1: (T1 invokes T2: (T2, version B)) 131 | // T0 (first version) invokes T1: (T1 invokes T2: (T2, version A)) 132 | } 133 | -------------------------------------------------------------------------------- /template/init.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package template 8 | 9 | import ( 10 | "github.com/google/safehtml/internal/template/raw" 11 | ) 12 | 13 | // The following functions are used by package uncheckedconversions 14 | // (via package raw) to create TrustedSource and TrustedTemplate values 15 | // from plain strings. 16 | 17 | func trustedSourceRaw(s string) TrustedSource { 18 | return TrustedSource{s} 19 | } 20 | 21 | func trustedTemplateRaw(s string) TrustedTemplate { 22 | return TrustedTemplate{s} 23 | } 24 | 25 | func init() { 26 | raw.TrustedSource = trustedSourceRaw 27 | raw.TrustedTemplate = trustedTemplateRaw 28 | } 29 | -------------------------------------------------------------------------------- /template/redefine_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template_test 6 | 7 | import ( 8 | "bytes" 9 | "strings" 10 | "testing" 11 | 12 | . "github.com/google/safehtml/template" 13 | ) 14 | 15 | func TestRedefineNonEmptyAfterExecution(t *testing.T) { 16 | c := newTestCase(t) 17 | c.mustParse(c.root, MakeTrustedTemplate(`foo`)) 18 | c.mustExecute(c.root, nil, "foo") 19 | c.mustNotParse(c.root, MakeTrustedTemplate(`bar`)) 20 | } 21 | 22 | func TestRedefineEmptyAfterExecution(t *testing.T) { 23 | c := newTestCase(t) 24 | c.mustParse(c.root, MakeTrustedTemplate(``)) 25 | c.mustExecute(c.root, nil, "") 26 | c.mustNotParse(c.root, MakeTrustedTemplate(`foo`)) 27 | c.mustExecute(c.root, nil, "") 28 | } 29 | 30 | func TestRedefineAfterNonExecution(t *testing.T) { 31 | c := newTestCase(t) 32 | c.mustParse(c.root, MakeTrustedTemplate(`{{if .}}<{{template "X"}}>{{end}}{{define "X"}}foo{{end}}`)) 33 | c.mustExecute(c.root, 0, "") 34 | c.mustNotParse(c.root, MakeTrustedTemplate(`{{define "X"}}bar{{end}}`)) 35 | c.mustExecute(c.root, 1, "<foo>") 36 | } 37 | 38 | func TestRedefineAfterNamedExecution(t *testing.T) { 39 | c := newTestCase(t) 40 | c.mustParse(c.root, MakeTrustedTemplate(`<{{template "X" .}}>{{define "X"}}foo{{end}}`)) 41 | c.mustExecute(c.root, nil, "<foo>") 42 | c.mustNotParse(c.root, MakeTrustedTemplate(`{{define "X"}}bar{{end}}`)) 43 | c.mustExecute(c.root, nil, "<foo>") 44 | } 45 | 46 | func TestRedefineNestedByNameAfterExecution(t *testing.T) { 47 | c := newTestCase(t) 48 | c.mustParse(c.root, MakeTrustedTemplate(`{{define "X"}}foo{{end}}`)) 49 | c.mustExecute(c.lookup("X"), nil, "foo") 50 | c.mustNotParse(c.root, MakeTrustedTemplate(`{{define "X"}}bar{{end}}`)) 51 | c.mustExecute(c.lookup("X"), nil, "foo") 52 | } 53 | 54 | func TestRedefineNestedByTemplateAfterExecution(t *testing.T) { 55 | c := newTestCase(t) 56 | c.mustParse(c.root, MakeTrustedTemplate(`{{define "X"}}foo{{end}}`)) 57 | c.mustExecute(c.lookup("X"), nil, "foo") 58 | c.mustNotParse(c.lookup("X"), MakeTrustedTemplate(`bar`)) 59 | c.mustExecute(c.lookup("X"), nil, "foo") 60 | } 61 | 62 | func TestRedefineSafety(t *testing.T) { 63 | c := newTestCase(t) 64 | c.mustParse(c.root, MakeTrustedTemplate(`{{define "X"}}{{end}}`)) 65 | c.mustExecute(c.root, nil, ``) 66 | // Note: Every version of Go prior to Go 1.8 accepted the redefinition of "X" 67 | // on the next line, but luckily kept it from being used in the outer template. 68 | // Now we reject it, which makes clearer that we're not going to use it. 69 | c.mustNotParse(c.root, MakeTrustedTemplate(`{{define "X"}}" bar="baz{{end}}`)) 70 | c.mustExecute(c.root, nil, ``) 71 | } 72 | 73 | func TestRedefineTopUse(t *testing.T) { 74 | c := newTestCase(t) 75 | c.mustParse(c.root, MakeTrustedTemplate(`{{template "X"}}{{.}}{{define "X"}}{{end}}`)) 76 | c.mustExecute(c.root, 42, `42`) 77 | c.mustNotParse(c.root, MakeTrustedTemplate(`{{define "X"}}`, 19 | )) 20 | 21 | // Using safehtml.Script to safely inject script content 22 | func Example_script() { 23 | err := tmplMyPage.Execute(os.Stdout, MyPageData{ 24 | Message: "welcome to my cool website!!", 25 | Script: safehtml.ScriptFromConstant(`alert("hello world!")`), 26 | }) 27 | 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // Output: 33 | // welcome to my cool website!! 34 | } 35 | -------------------------------------------------------------------------------- /template/state_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type State"; DO NOT EDIT 2 | 3 | package template 4 | 5 | import "fmt" 6 | 7 | const _State_name = "StateTextStateSpecialElementBodyStateTagStateAttrNameStateAfterNameStateBeforeValueStateHTMLCmtStateAttrStateError" 8 | 9 | var _State_index = [...]uint16{0, 9, 32, 40, 53, 67, 83, 95, 104, 114} 10 | 11 | func (i state) String() string { 12 | if i >= state(len(_State_index)-1) { 13 | return fmt.Sprintf("state(%d)", i) 14 | } 15 | return _State_name[_State_index[i]:_State_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /template/template_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "bytes" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | "testing" 16 | ) 17 | 18 | const tmplText = "foo" 19 | 20 | func TestParseExecute(t *testing.T) { 21 | tmpl := New("test") 22 | parsedTmpl := Must(tmpl.Parse(tmplText)) 23 | if parsedTmpl != tmpl { 24 | t.Errorf("expected Parse to update template") 25 | } 26 | var buf bytes.Buffer 27 | if err := tmpl.Execute(&buf, nil); err != nil { 28 | t.Fatalf(err.Error()) 29 | } 30 | if buf.String() != tmplText { 31 | t.Errorf("expected %s got %s", tmplText, buf.String()) 32 | } 33 | } 34 | 35 | func TestMustParseAndExecuteToHTML(t *testing.T) { 36 | for _, test := range [...]struct { 37 | text stringConstant 38 | want string 39 | }{ 40 | { 41 | `hello world!`, 42 | `hello world!`, 43 | }, 44 | { 45 | `all we need is <3`, 46 | `all we need is <3`, 47 | }, 48 | } { 49 | html := MustParseAndExecuteToHTML(test.text) 50 | if got := html.String(); got != string(test.want) { 51 | t.Errorf("MustParseAndExecuteToHTML(%q) = %q, want %q", string(test.text), got, test.want) 52 | } 53 | } 54 | } 55 | 56 | func TestTemplateClone(t *testing.T) { 57 | // https://golang.org/issue/12996 58 | orig := New("name") 59 | clone, err := orig.Clone() 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | if len(clone.Templates()) != len(orig.Templates()) { 64 | t.Fatalf("Invalid length of t.Clone().Templates()") 65 | } 66 | 67 | const want = "stuff" 68 | parsed := Must(clone.Parse(want)) 69 | var buf bytes.Buffer 70 | err = parsed.Execute(&buf, nil) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | if got := buf.String(); got != want { 75 | t.Fatalf("got %q; want %q", got, want) 76 | } 77 | } 78 | 79 | const ( 80 | tmpl1 = `{{define "a"}}foo{{end}}` 81 | tmpl2 = `{{define "b"}}bar{{end}}` 82 | ) 83 | 84 | func TestLookup(t *testing.T) { 85 | tmpl := Must(New("test").Parse(tmpl1)) 86 | Must(tmpl.Parse(tmpl2)) 87 | a := tmpl.Lookup("a") 88 | if a == nil || a.Name() != "a" { 89 | t.Errorf("lookup on a failed") 90 | } 91 | b := tmpl.Lookup("b") 92 | if b == nil || b.Name() != "b" { 93 | t.Errorf("lookup on b failed") 94 | } 95 | if tmpl.Lookup("c") != nil { 96 | t.Errorf("lookup returned non-nil value for undefined template c") 97 | } 98 | } 99 | 100 | func TestTemplates(t *testing.T) { 101 | // want maps template name to expected output. 102 | want := map[string]string{ 103 | "test": "", 104 | "a": "foo", 105 | "b": "bar", 106 | } 107 | tmpl := Must(New("test").Parse(tmpl1)) 108 | Must(tmpl.Parse(tmpl2)) 109 | templates := tmpl.Templates() 110 | if len(templates) != len(want) { 111 | t.Fatalf("want %d templates, got %d", len(want), len(templates)) 112 | } 113 | for name := range want { 114 | found := false 115 | for _, tmpl := range templates { 116 | if name == tmpl.text.Name() { 117 | found = true 118 | break 119 | } 120 | } 121 | if !found { 122 | t.Error("could not find template", name) 123 | } 124 | } 125 | for _, got := range templates { 126 | name := got.Name() 127 | wantOutput, ok := want[name] 128 | if !ok { 129 | t.Errorf("got unexpected template name %q", name) 130 | } 131 | var buf bytes.Buffer 132 | if err := got.Execute(&buf, nil); err != nil { 133 | t.Fatalf("template %q: error executing: %v", name, err) 134 | } 135 | if buf.String() != wantOutput { 136 | t.Errorf("template %q: want output %s, got %s", name, wantOutput, buf.String()) 137 | } 138 | } 139 | } 140 | 141 | func createTestDirAndFile(filename string) string { 142 | dir, err := ioutil.TempDir("", "template") 143 | if err != nil { 144 | log.Fatal(err) 145 | } 146 | f, err := os.Create(filepath.Join(dir, filename)) 147 | if err != nil { 148 | log.Fatal(err) 149 | } 150 | defer f.Close() 151 | _, err = io.WriteString(f, "Test template contents") 152 | if err != nil { 153 | log.Fatal(err) 154 | } 155 | return dir 156 | } 157 | 158 | const filename = "T1.tmpl" 159 | 160 | func TestParseFiles(t *testing.T) { 161 | dir := createTestDirAndFile(filename) 162 | tmpl := New("root") 163 | parsedTmpl := Must(tmpl.ParseFiles(stringConstant(filepath.Join(dir, filename)))) 164 | if parsedTmpl != tmpl { 165 | t.Errorf("expected ParseFiles to update template") 166 | } 167 | } 168 | 169 | func TestParseGlob(t *testing.T) { 170 | dir := createTestDirAndFile(filename) 171 | tmpl := New("root") 172 | parsedTmpl := Must(tmpl.ParseGlob(stringConstant(filepath.Join(dir, "T*.tmpl")))) 173 | if parsedTmpl != tmpl { 174 | t.Errorf("expected ParseGlob to update template") 175 | } 176 | } 177 | 178 | func TestDontAllowJSTemplateSubstitution(t *testing.T) { 179 | const template = "" 180 | templ := Must(New("foo").Parse(template)) 181 | var b bytes.Buffer 182 | err := templ.Execute(&b, "foo") 183 | const want = "must be balanced" 184 | if err == nil || strings.Contains(err.Error(), "want") { 185 | t.Errorf("Parsed template %v, got error %v, expected %v", template, err, want) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /template/testdata/dir1/parsefiles_t1.tmpl: -------------------------------------------------------------------------------- 1 | T1 invokes T2: ({{template "T2"}}) -------------------------------------------------------------------------------- /template/testdata/dir2/parsefiles_t2.tmpl: -------------------------------------------------------------------------------- 1 | {{define "T2"}}This is T2{{end}} -------------------------------------------------------------------------------- /template/testdata/glob_t0.tmpl: -------------------------------------------------------------------------------- 1 | T0 invokes T1: ({{template "T1"}}) -------------------------------------------------------------------------------- /template/testdata/glob_t1.tmpl: -------------------------------------------------------------------------------- 1 | {{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}} -------------------------------------------------------------------------------- /template/testdata/glob_t2.tmpl: -------------------------------------------------------------------------------- 1 | {{define "T2"}}This is T2{{end}} -------------------------------------------------------------------------------- /template/testdata/helpers_t1.tmpl: -------------------------------------------------------------------------------- 1 | {{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}} -------------------------------------------------------------------------------- /template/testdata/helpers_t2.tmpl: -------------------------------------------------------------------------------- 1 | {{define "T2"}}This is T2{{end}} -------------------------------------------------------------------------------- /template/testdata/share_t0.tmpl: -------------------------------------------------------------------------------- 1 | T0 ({{.}} version) invokes T1: ({{template `T1`}}) 2 | -------------------------------------------------------------------------------- /template/testdata/share_t1.tmpl: -------------------------------------------------------------------------------- 1 | {{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}} -------------------------------------------------------------------------------- /template/transition.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "bytes" 9 | "strings" 10 | ) 11 | 12 | // transitionFunc is the array of context transition functions for text nodes. 13 | // A transition function takes a context and template text input, and returns 14 | // the updated context and the number of bytes consumed from the front of the 15 | // input. 16 | var transitionFunc = [...]func(context, []byte) (context, int){ 17 | stateText: tText, 18 | stateSpecialElementBody: tSpecialTagEnd, 19 | stateTag: tTag, 20 | stateAttrName: tAttrName, 21 | stateAfterName: tAfterName, 22 | stateBeforeValue: tBeforeValue, 23 | stateHTMLCmt: tHTMLCmt, 24 | stateAttr: tAttr, 25 | stateError: tError, 26 | } 27 | 28 | var commentStart = []byte("") 30 | 31 | // tText is the context transition function for the text state. 32 | func tText(c context, s []byte) (context, int) { 33 | k := 0 34 | for { 35 | i := k + bytes.IndexByte(s[k:], '<') 36 | if i < k || i+1 == len(s) { 37 | return c, len(s) 38 | } else if i+4 <= len(s) && bytes.Equal(commentStart, s[i:i+4]) { 39 | return context{state: stateHTMLCmt}, i + 4 40 | } 41 | i++ 42 | end := false 43 | if s[i] == '/' { 44 | if i+1 == len(s) { 45 | return c, len(s) 46 | } 47 | end, i = true, i+1 48 | } 49 | j, e := eatTagName(s, i) 50 | if j != i { 51 | // We've found an HTML tag. 52 | ret := context{state: stateTag} 53 | // Element name not needed if we are at the end of the element. 54 | if !end { 55 | ret.element = e 56 | } 57 | return ret, j 58 | } 59 | k = j 60 | } 61 | } 62 | 63 | // specialElements contains the names of elements whose bodies are treated 64 | // differently by the parser and escaper from stateText. 65 | var specialElements = map[string]bool{ 66 | "script": true, 67 | "style": true, 68 | "textarea": true, 69 | "title": true, 70 | } 71 | 72 | // voidElements contains the names of all void elements. 73 | // https://www.w3.org/TR/html5/syntax.html#void-elements 74 | var voidElements = map[string]bool{ 75 | "area": true, 76 | "base": true, 77 | "br": true, 78 | "col": true, 79 | "embed": true, 80 | "hr": true, 81 | "img": true, 82 | "input": true, 83 | "keygen": true, 84 | "link": true, 85 | "meta": true, 86 | "param": true, 87 | "source": true, 88 | "track": true, 89 | "wbr": true, 90 | } 91 | 92 | // tTag is the context transition function for the tag state. 93 | func tTag(c context, s []byte) (context, int) { 94 | // Find the attribute name. 95 | i := eatWhiteSpace(s, 0) 96 | if i == len(s) { 97 | return c, len(s) 98 | } 99 | if s[i] == '>' { 100 | ret := context{ 101 | state: stateText, 102 | element: c.element, 103 | scriptType: c.scriptType, 104 | linkRel: c.linkRel, 105 | } 106 | if specialElements[c.element.name] { 107 | ret.state = stateSpecialElementBody 108 | } 109 | if c.element.name != "" && voidElements[c.element.name] { 110 | // Special case: end of start tag of a void element. 111 | // Discard unnecessary state, since this element have no content. 112 | ret.element = element{} 113 | ret.scriptType = "" 114 | ret.linkRel = "" 115 | } 116 | return ret, i + 1 117 | } 118 | j, err := eatAttrName(s, i) 119 | if err != nil { 120 | return context{state: stateError, err: err}, len(s) 121 | } 122 | state := stateTag 123 | if i == j { 124 | return context{ 125 | state: stateError, 126 | err: errorf(ErrBadHTML, nil, 0, "expected space, attr name, or end of tag, but got %q", s[i:]), 127 | }, len(s) 128 | } 129 | 130 | if j == len(s) { 131 | state = stateAttrName 132 | } else { 133 | state = stateAfterName 134 | } 135 | return context{ 136 | state: state, 137 | element: c.element, 138 | attr: attr{name: strings.ToLower(string(s[i:j]))}, 139 | linkRel: c.linkRel, 140 | }, j 141 | } 142 | 143 | // tAttrName is the context transition function for stateAttrName. 144 | func tAttrName(c context, s []byte) (context, int) { 145 | i, err := eatAttrName(s, 0) 146 | if err != nil { 147 | return context{state: stateError, err: err}, len(s) 148 | } else if i != len(s) { 149 | c.state = stateAfterName 150 | } 151 | return c, i 152 | } 153 | 154 | // tAfterName is the context transition function for stateAfterName. 155 | func tAfterName(c context, s []byte) (context, int) { 156 | // Look for the start of the value. 157 | i := eatWhiteSpace(s, 0) 158 | if i == len(s) { 159 | return c, len(s) 160 | } else if s[i] != '=' { 161 | // Occurs due to tag ending '>', and valueless attribute. 162 | c.state = stateTag 163 | return c, i 164 | } 165 | c.state = stateBeforeValue 166 | // Consume the "=". 167 | return c, i + 1 168 | } 169 | 170 | // tBeforeValue is the context transition function for stateBeforeValue. 171 | func tBeforeValue(c context, s []byte) (context, int) { 172 | i := eatWhiteSpace(s, 0) 173 | if i == len(s) { 174 | return c, len(s) 175 | } 176 | // Find the attribute delimiter. 177 | // TODO: consider disallowing single-quoted or unquoted attribute values completely, even in hardcoded template text. 178 | delim := delimSpaceOrTagEnd 179 | switch s[i] { 180 | case '\'': 181 | delim, i = delimSingleQuote, i+1 182 | case '"': 183 | delim, i = delimDoubleQuote, i+1 184 | } 185 | c.state, c.delim = stateAttr, delim 186 | return c, i 187 | } 188 | 189 | // tHTMLCmt is the context transition function for stateHTMLCmt. 190 | func tHTMLCmt(c context, s []byte) (context, int) { 191 | if i := bytes.Index(s, commentEnd); i != -1 { 192 | return context{}, i + 3 193 | } 194 | return c, len(s) 195 | } 196 | 197 | var ( 198 | specialTagEndPrefix = []byte(" \t\n\f/") 200 | ) 201 | 202 | // tSpecialTagEnd is the context transition function for raw text, RCDATA 203 | // script data, and stylesheet element states. 204 | func tSpecialTagEnd(c context, s []byte) (context, int) { 205 | if specialElements[c.element.name] { 206 | if i := indexTagEnd(s, []byte(c.element.name)); i != -1 { 207 | return context{}, i 208 | } 209 | } 210 | return c, len(s) 211 | } 212 | 213 | // indexTagEnd finds the index of a special tag end in a case insensitive way, or returns -1 214 | func indexTagEnd(s []byte, tag []byte) int { 215 | res := 0 216 | plen := len(specialTagEndPrefix) 217 | for len(s) > 0 { 218 | // Try to find the tag end prefix first 219 | i := bytes.Index(s, specialTagEndPrefix) 220 | if i == -1 { 221 | return i 222 | } 223 | s = s[i+plen:] 224 | // Try to match the actual tag if there is still space for it 225 | if len(tag) <= len(s) && bytes.EqualFold(tag, s[:len(tag)]) { 226 | s = s[len(tag):] 227 | // Check the tag is followed by a proper separator 228 | if len(s) > 0 && bytes.IndexByte(tagEndSeparators, s[0]) != -1 { 229 | return res + i 230 | } 231 | res += len(tag) 232 | } 233 | res += i + plen 234 | } 235 | return -1 236 | } 237 | 238 | // tAttr is the context transition function for the attribute state. 239 | func tAttr(c context, s []byte) (context, int) { 240 | return c, len(s) 241 | } 242 | 243 | // tError is the context transition function for the error state. 244 | func tError(c context, s []byte) (context, int) { 245 | return c, len(s) 246 | } 247 | 248 | // eatAttrName returns the largest j such that s[i:j] is an attribute name. 249 | // It returns an error if s[i:] does not look like it begins with an 250 | // attribute name, such as encountering a quote mark without a preceding 251 | // equals sign. 252 | func eatAttrName(s []byte, i int) (int, *Error) { 253 | for j := i; j < len(s); j++ { 254 | switch s[j] { 255 | case ' ', '\t', '\n', '\f', '\r', '=', '>': 256 | return j, nil 257 | case '\'', '"', '<': 258 | // These result in a parse warning in HTML5 and are 259 | // indicative of serious problems if seen in an attr 260 | // name in a template. 261 | return -1, errorf(ErrBadHTML, nil, 0, "%q in attribute name: %.32q", s[j:j+1], s) 262 | default: 263 | // No-op. 264 | } 265 | } 266 | return len(s), nil 267 | } 268 | 269 | // asciiAlpha reports whether c is an ASCII letter. 270 | func asciiAlpha(c byte) bool { 271 | return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' 272 | } 273 | 274 | // asciiAlphaNum reports whether c is an ASCII letter or digit. 275 | func asciiAlphaNum(c byte) bool { 276 | return asciiAlpha(c) || '0' <= c && c <= '9' 277 | } 278 | 279 | // eatTagName returns the largest j such that s[i:j] is a tag name and the tag name. 280 | func eatTagName(s []byte, i int) (int, element) { 281 | if i == len(s) || !asciiAlpha(s[i]) { 282 | return i, element{} 283 | } 284 | j := i + 1 285 | for j < len(s) { 286 | x := s[j] 287 | if asciiAlphaNum(x) { 288 | j++ 289 | continue 290 | } 291 | // Allow "x-y" or "x:y" but not "x-", "-y", or "x--y". 292 | if (x == ':' || x == '-') && j+1 < len(s) && asciiAlphaNum(s[j+1]) { 293 | j += 2 294 | continue 295 | } 296 | break 297 | } 298 | return j, element{name: strings.ToLower(string(s[i:j]))} 299 | } 300 | 301 | // eatWhiteSpace returns the largest j such that s[i:j] is white space. 302 | func eatWhiteSpace(s []byte, i int) int { 303 | for j := i; j < len(s); j++ { 304 | switch s[j] { 305 | case ' ', '\t', '\n', '\f', '\r': 306 | // No-op. 307 | default: 308 | return j 309 | } 310 | } 311 | return len(s) 312 | } 313 | -------------------------------------------------------------------------------- /template/transition_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "bytes" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestFindEndTag(t *testing.T) { 14 | tests := []struct { 15 | s, tag string 16 | want int 17 | }{ 18 | {"", "tag", -1}, 19 | {"hello hello", "textarea", 6}, 20 | {"hello hello", "textarea", 6}, 21 | {"hello ", "textarea", 6}, 22 | {"hello ", "tag", -1}, 24 | {"hello tag ", "textarea", 22}, 26 | {" ", "textarea", 0}, 27 | {"
", "textarea", 13}, 28 | {"
", "textarea", 13}, 29 | {"
", "textarea", 13}, 30 | {"
", "textarea", 14}, 32 | {"<", "script", 1}, 33 | {"", "textarea", -1}, 34 | } 35 | for _, test := range tests { 36 | if got := indexTagEnd([]byte(test.s), []byte(test.tag)); test.want != got { 37 | t.Errorf("%q/%q: want\n\t%d\nbut got\n\t%d", test.s, test.tag, test.want, got) 38 | } 39 | } 40 | } 41 | 42 | func BenchmarkTemplateSpecialTags(b *testing.B) { 43 | 44 | r := struct { 45 | Name, Gift string 46 | }{"Aunt Mildred", "bone china tea set"} 47 | 48 | h1 := " " 49 | h2 := "" 50 | html := strings.Repeat(h1, 100) + h2 + strings.Repeat(h1, 100) + h2 51 | 52 | var buf bytes.Buffer 53 | for i := 0; i < b.N; i++ { 54 | tmpl := Must(New("foo").Parse(stringConstant(html))) 55 | if err := tmpl.Execute(&buf, r); err != nil { 56 | b.Fatal(err) 57 | } 58 | buf.Reset() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /template/trustedfs.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | //go:build go1.16 8 | // +build go1.16 9 | 10 | package template 11 | 12 | import ( 13 | "embed" 14 | "fmt" 15 | "io/fs" 16 | "os" 17 | "path" 18 | ) 19 | 20 | // A TrustedFS is an immutable type referencing a filesystem (fs.FS) 21 | // under application control. 22 | // 23 | // In order to ensure that an attacker cannot influence the TrustedFS value, a 24 | // TrustedFS can be instantiated in only two ways. One way is from an embed.FS 25 | // with TrustedFSFromEmbed. It is assumed that embedded filesystems are under 26 | // the programmer's control. The other way is from a TrustedSource using 27 | // TrustedFSFromTrustedSource, in which case the guarantees and caveats of 28 | // TrustedSource apply. 29 | type TrustedFS struct { 30 | fsys fs.FS 31 | } 32 | 33 | // TrustedFSFromEmbed constructs a TrustedFS from an embed.FS. 34 | func TrustedFSFromEmbed(fsys embed.FS) TrustedFS { 35 | return TrustedFS{fsys: fsys} 36 | } 37 | 38 | // TrustedFSFromTrustedSource constructs a TrustedFS from the string in the 39 | // TrustedSource, which should refer to a directory. 40 | func TrustedFSFromTrustedSource(ts TrustedSource) TrustedFS { 41 | return TrustedFS{fsys: os.DirFS(ts.src)} 42 | } 43 | 44 | // Sub returns a TrustedFS at a subdirectory of the receiver. 45 | // It works by calling fs.Sub on the receiver's fs.FS. 46 | func (tf TrustedFS) Sub(dir TrustedSource) (TrustedFS, error) { 47 | subfs, err := fs.Sub(tf.fsys, dir.String()) 48 | return TrustedFS{fsys: subfs}, err 49 | } 50 | 51 | // ParseFS is like ParseFiles or ParseGlob but reads from the TrustedFS 52 | // instead of the host operating system's file system. 53 | // It accepts a list of glob patterns. 54 | // (Note that most file names serve as glob patterns matching only themselves.) 55 | // 56 | // The same behaviors listed for ParseFiles() apply to ParseFS too (e.g. using the base name 57 | // of the file as the template name). 58 | func ParseFS(tfs TrustedFS, patterns ...string) (*Template, error) { 59 | return parseFS(nil, tfs.fsys, patterns) 60 | } 61 | 62 | // ParseFS is like ParseFiles or ParseGlob but reads from the TrustedFS 63 | // instead of the host operating system's file system. 64 | // It accepts a list of glob patterns. 65 | // (Note that most file names serve as glob patterns matching only themselves.) 66 | // 67 | // The same behaviors listed for ParseFiles() apply to ParseFS too (e.g. using the base name 68 | // of the file as the template name). 69 | func (t *Template) ParseFS(tfs TrustedFS, patterns ...string) (*Template, error) { 70 | return parseFS(t, tfs.fsys, patterns) 71 | } 72 | 73 | // Copied from 74 | // https://go.googlesource.com/go/+/refs/tags/go1.17.1/src/text/template/helper.go. 75 | func parseFS(t *Template, fsys fs.FS, patterns []string) (*Template, error) { 76 | var filenames []string 77 | for _, pattern := range patterns { 78 | list, err := fs.Glob(fsys, pattern) 79 | if err != nil { 80 | return nil, err 81 | } 82 | if len(list) == 0 { 83 | return nil, fmt.Errorf("template: pattern matches no files: %#q", pattern) 84 | } 85 | filenames = append(filenames, list...) 86 | } 87 | return parseFiles(t, readFileFS(fsys), filenames...) 88 | } 89 | 90 | // Copied with minor changes from 91 | // https://go.googlesource.com/go/+/refs/tags/go1.17.1/src/text/template/helper.go. 92 | func readFileFS(fsys fs.FS) func(string) (string, []byte, error) { 93 | return func(file string) (string, []byte, error) { 94 | name := path.Base(file) 95 | b, err := fs.ReadFile(fsys, file) 96 | return name, b, err 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /template/trustedfs_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | //go:build go1.16 8 | // +build go1.16 9 | 10 | package template 11 | 12 | import ( 13 | "embed" 14 | "testing" 15 | ) 16 | 17 | //go:embed testdata 18 | var testFS embed.FS 19 | 20 | func TestParseFS(t *testing.T) { 21 | tmpl := New("root") 22 | parsedTmpl := Must(tmpl.ParseFS(TrustedFSFromEmbed(testFS), "testdata/glob_*.tmpl")) 23 | if parsedTmpl != tmpl { 24 | t.Errorf("expected ParseFS to update template") 25 | } 26 | } 27 | 28 | func TestSub(t *testing.T) { 29 | tfs := TrustedFSFromEmbed(testFS) 30 | sub, err := tfs.Sub(TrustedSourceFromConstant("testdata")) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | tmpl := New("t1") 35 | parsedTmpl := Must(tmpl.ParseFS(sub, "dir1/parsefiles_t1.tmpl")) 36 | if parsedTmpl != tmpl { 37 | t.Errorf("expected ParseFS to update template") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /template/trustedsource.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package template 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | 15 | "flag" 16 | ) 17 | 18 | // A TrustedSource is an immutable string-like type referencing 19 | // trusted template files under application control. It can be passed to 20 | // template-parsing functions and methods to safely load templates 21 | // without the risk of untrusted template execution. 22 | // 23 | // In order to ensure that an attacker cannot influence the TrustedSource 24 | // value, a TrustedSource can be instantiated only from untyped string 25 | // constants, command-line flags, and other application-controlled strings, but 26 | // never from arbitrary string values potentially representing untrusted user input. 27 | // 28 | // Note that TrustedSource's constructors cannot truly guarantee that the 29 | // templates it references are not attacker-controlled; it can guarantee only that 30 | // the path to the template itself is under application control. Users of these 31 | // constructors must ensure themselves that TrustedSource never references 32 | // attacker-controlled files or directories that contain such files. 33 | type TrustedSource struct { 34 | // We declare a TrustedSource not as a string but as a struct wrapping a string 35 | // to prevent construction of TrustedSource values through string conversion. 36 | src string 37 | } 38 | 39 | // TrustedSourceFromConstant constructs a TrustedSource with its underlying 40 | // src set to the given src, which must be an untyped string constant. 41 | // 42 | // No runtime validation or sanitization is performed on src; being under 43 | // application control, it is simply assumed to comply with the TrustedSource type 44 | // contract. 45 | func TrustedSourceFromConstant(src stringConstant) TrustedSource { 46 | return TrustedSource{string(src)} 47 | } 48 | 49 | // TrustedSourceFromConstantDir constructs a TrustedSource calling path/filepath.Join on 50 | // an application-controlled directory path, which must be an untyped string constant, 51 | // a TrustedSource, and a dynamic filename. It returns an error if filename contains 52 | // filepath or list separators, since this might cause the resulting path to reference a 53 | // file outside of the given directory. 54 | // 55 | // dir or src may be empty if either of these path segments are not required. 56 | func TrustedSourceFromConstantDir(dir stringConstant, src TrustedSource, filename string) (TrustedSource, error) { 57 | if i := strings.IndexAny(filename, string([]rune{filepath.Separator, filepath.ListSeparator})); i != -1 { 58 | return TrustedSource{}, fmt.Errorf("filename %q must not contain the separator %q", filename, filename[i]) 59 | } 60 | if filename == ".." { 61 | return TrustedSource{}, fmt.Errorf("filename must not be the special name %q", filename) 62 | } 63 | return TrustedSource{filepath.Join(string(dir), src.String(), filename)}, nil 64 | } 65 | 66 | // TrustedSourceJoin is a wrapper around path/filepath.Join that returns a 67 | // TrustedSource formed by joining the given path elements into a single path, 68 | // adding an OS-specific path separator if necessary. 69 | func TrustedSourceJoin(elem ...TrustedSource) TrustedSource { 70 | return TrustedSource{filepath.Join(trustedSourcesToStrings(elem)...)} 71 | } 72 | 73 | // TrustedSourceFromFlag returns a TrustedSource containing the string 74 | // representation of the retrieved value of the flag. 75 | // 76 | // In a server setting, flags are part of the application's deployment 77 | // configuration and are hence considered application-controlled. 78 | func TrustedSourceFromFlag(value flag.Value) TrustedSource { 79 | return TrustedSource{fmt.Sprint(value.String())} 80 | } 81 | 82 | // TrustedSourceFromEnvVar is a wrapper around os.Getenv that 83 | // returns a TrustedSource containing the value of the environment variable 84 | // named by the key. It returns the value, which will be empty if the variable 85 | // is not present. To distinguish between an empty value and an unset value, 86 | // use os.LookupEnv. 87 | // 88 | // In a server setting, environment variables are part of the application's 89 | // deployment configuration and are hence considered application-controlled. 90 | func TrustedSourceFromEnvVar(key stringConstant) TrustedSource { 91 | return TrustedSource{os.Getenv(string(key))} 92 | } 93 | 94 | // String returns the string form of the TrustedSource. 95 | func (t TrustedSource) String() string { 96 | return t.src 97 | } 98 | 99 | func trustedSourcesToStrings(paths []TrustedSource) []string { 100 | ret := make([]string, 0, len(paths)) 101 | for _, p := range paths { 102 | ret = append(ret, p.String()) 103 | } 104 | return ret 105 | } 106 | -------------------------------------------------------------------------------- /template/trustedsource_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package template 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | func TestTrustedSourceFromConstant(t *testing.T) { 16 | const want = `foo` 17 | if got := TrustedSourceFromConstant(want).String(); got != want { 18 | t.Errorf("got: %q, want: %q", got, want) 19 | } 20 | } 21 | 22 | func TestTrustedSourceFromConstantDir(t *testing.T) { 23 | // Use a short alias to make test cases more readable. 24 | c := TrustedSourceFromConstant 25 | for _, test := range [...]struct { 26 | dir string 27 | src TrustedSource 28 | filename, want, err string 29 | }{ 30 | {"foo/", c(""), "file", "foo/file", ""}, 31 | {"foo/", TrustedSource{}, "file", "foo/file", ""}, 32 | {"", c("foo/"), "file", "foo/file", ""}, 33 | {"foo", c("bar"), "file", "foo/bar/file", ""}, 34 | {"foo/bar", c("baz"), "file", "foo/bar/baz/file", ""}, 35 | {"foo/bar", c("baz"), "file.html", "foo/bar/baz/file.html", ""}, 36 | {"foo", c("bar"), "dir:otherPath", "", `filename "dir:otherPath" must not contain the separator ':'`}, 37 | {"foo", c("bar"), "dir/file.html", "", `filename "dir/file.html" must not contain the separator '/'`}, 38 | {"foo", c("bar"), "../file.html", "", `filename "../file.html" must not contain the separator '/'`}, 39 | {"foo/bar", c("baz"), "..", "", `filename must not be the special name ".."`}, 40 | } { 41 | ts, err := TrustedSourceFromConstantDir(stringConstant(test.dir), test.src, test.filename) 42 | prefix := fmt.Sprintf("dir %q src %q filename %q", test.dir, test.src, test.filename) 43 | if test.err == "" && err != nil { 44 | t.Errorf("%s : unexpected error: %s", prefix, err) 45 | } else if test.err != "" && err == nil { 46 | t.Errorf("%s : expected error", prefix) 47 | } else if test.err != "" && err.Error() != test.err { 48 | t.Errorf("%s : got error:\n\t%s\nwant:\n\t%s", prefix, err, test.err) 49 | } else if ts.String() != test.want { 50 | t.Errorf("%s : got %q, want %q", prefix, ts.String(), test.want) 51 | } 52 | } 53 | } 54 | 55 | func TestTrustedSourceJoin(t *testing.T) { 56 | // Use a short alias to make test cases more readable. 57 | c := TrustedSourceFromConstant 58 | for _, test := range [...]struct { 59 | desc string 60 | in []TrustedSource 61 | want string 62 | }{ 63 | {"Path separators added if necessary", 64 | []TrustedSource{c("foo"), c("bar/"), c("/baz"), c("/far")}, 65 | "foo/bar/baz/far", 66 | }, 67 | 68 | {".. path segments formed by concatenating multiple TrustedSource values do not affect the resultant path", 69 | []TrustedSource{c("foo"), c("bar/."), c("./baz")}, 70 | "foo/bar/baz", 71 | }, 72 | { 73 | ".. path segments that are explicitly specified in individual TrustedSource values will take effect", 74 | []TrustedSource{c("foo"), c("bar"), c("baz/.."), c("../far")}, 75 | "foo/far", 76 | }, 77 | } { 78 | if got := TrustedSourceJoin(test.in...).String(); got != test.want { 79 | t.Errorf("%s : got: %q, want: %q", test.desc, got, test.want) 80 | } 81 | } 82 | } 83 | 84 | type testFlagValue string 85 | 86 | func (t *testFlagValue) String() string { return string(*t) } 87 | 88 | func (t *testFlagValue) Get() interface{} { return *t } 89 | 90 | func (t *testFlagValue) Set(s string) error { 91 | *t = testFlagValue(s) 92 | return nil 93 | } 94 | 95 | func TestTrustedSourceFromFlag(t *testing.T) { 96 | const want = `foo` 97 | value := testFlagValue(want) 98 | if got := TrustedSourceFromFlag(&value).String(); got != want { 99 | t.Errorf("got: %q, want: %q", got, want) 100 | } 101 | } 102 | 103 | func TestTrustedSourceFromEnvVar(t *testing.T) { 104 | const tmpDirEnvVar = `TMPDIR` 105 | const want = `/my/tmp` 106 | os.Setenv(tmpDirEnvVar, want) 107 | defer os.Unsetenv(tmpDirEnvVar) 108 | if got := TrustedSourceFromEnvVar(tmpDirEnvVar).String(); got != want { 109 | t.Errorf("got: %q, want: %q", got, want) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /template/trustedtemplate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package template 8 | 9 | // A TrustedTemplate is an immutable string-like type containing a 10 | // safehtml/template template body. It can be safely loaded as template 11 | // text without the risk of untrusted template execution. 12 | // 13 | // In order to ensure that an attacker cannot influence the TrustedTemplate 14 | // value, a TrustedTemplate can be instantiated only from untyped string constants, 15 | // and never from arbitrary string values potentially representing untrusted user input. 16 | type TrustedTemplate struct { 17 | // We declare a TrustedTemplate not as a string but as a struct wrapping a string 18 | // to prevent construction of TrustedTemplate values through string conversion. 19 | tmpl string 20 | } 21 | 22 | // MakeTrustedTemplate constructs a TrustedTemplate with its underlying 23 | // tmpl set to the given tmpl, which must be an untyped string constant. 24 | // 25 | // No runtime validation or sanitization is performed on tmpl; being under 26 | // application control, it is simply assumed to comply with the TrustedTemplate type 27 | // contract. 28 | func MakeTrustedTemplate(tmpl stringConstant) TrustedTemplate { 29 | return TrustedTemplate{string(tmpl)} 30 | } 31 | 32 | // String returns the string form of the TrustedTemplate. 33 | func (t TrustedTemplate) String() string { 34 | return t.tmpl 35 | } 36 | -------------------------------------------------------------------------------- /template/trustedtemplate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package template 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestMakeTrustedTemplate(t *testing.T) { 14 | const want = `foo` 15 | if got := MakeTrustedTemplate(want).String(); got != want { 16 | t.Errorf("got: %q, want: %q", got, want) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /template/uncheckedconversions/uncheckedconversions.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // Package uncheckedconversions provides functions to create values of 8 | // safehtml/template types from plain strings. Use of these 9 | // functions could potentially result in safehtml/template type values that 10 | // violate their type contract, and hence result in security vulnerabilties. 11 | package uncheckedconversions 12 | 13 | import ( 14 | "github.com/google/safehtml/internal/template/raw" 15 | "github.com/google/safehtml/template" 16 | ) 17 | 18 | var trustedSource = raw.TrustedSource.(func(string) template.TrustedSource) 19 | var trustedTemplate = raw.TrustedTemplate.(func(string) template.TrustedTemplate) 20 | 21 | // TrustedSourceFromStringKnownToSatisfyTypeContract converts a string into a TrustedSource. 22 | func TrustedSourceFromStringKnownToSatisfyTypeContract(s string) template.TrustedSource { 23 | return trustedSource(s) 24 | } 25 | 26 | // TrustedTemplateFromStringKnownToSatisfyTypeContract converts a string into a TrustedTemplate. 27 | func TrustedTemplateFromStringKnownToSatisfyTypeContract(s string) template.TrustedTemplate { 28 | return trustedTemplate(s) 29 | } 30 | -------------------------------------------------------------------------------- /template/uncheckedconversions/uncheckedconversions_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package uncheckedconversions 8 | 9 | import ( 10 | "testing" 11 | ) 12 | 13 | func TestTrustedSourceFromStringKnownToSatisfyTypeContract(t *testing.T) { 14 | src := `some src` 15 | if out := TrustedSourceFromStringKnownToSatisfyTypeContract(src).String(); src != out { 16 | t.Errorf("uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q", 17 | src, out, src) 18 | } 19 | } 20 | 21 | func TestTrustedTemplateFromStringKnownToSatisfyTypeContract(t *testing.T) { 22 | tmpl := `some tmpl` 23 | if out := TrustedTemplateFromStringKnownToSatisfyTypeContract(tmpl).String(); tmpl != out { 24 | t.Errorf("uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(%q).String() = %q, want %q", 25 | tmpl, out, tmpl) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /template/url.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "fmt" 9 | "html" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/google/safehtml/internal/safehtmlutil" 14 | "github.com/google/safehtml" 15 | ) 16 | 17 | // urlPrefixValidators maps URL and TrustedResourceURL sanitization contexts to functions return an error 18 | // if the given string is unsafe to use as a URL prefix in that sanitization context. 19 | var urlPrefixValidators = map[sanitizationContext]func(string) error{ 20 | sanitizationContextURL: validateURLPrefix, 21 | sanitizationContextTrustedResourceURLOrURL: validateURLPrefix, 22 | sanitizationContextTrustedResourceURL: validateTrustedResourceURLPrefix, 23 | } 24 | 25 | // startsWithFullySpecifiedSchemePattern matches strings that have a fully-specified scheme component. 26 | // See RFC 3986 Section 3. 27 | var startsWithFullySpecifiedSchemePattern = regexp.MustCompile( 28 | `^[[:alpha:]](?:[[:alnum:]]|[+.-])*:`) 29 | 30 | // validateURLPrefix validates if the given non-empty prefix is a safe safehtml.URL prefix. 31 | // 32 | // Prefixes are considered unsafe if they end in an incomplete HTML character reference 33 | // or percent-encoding character triplet. 34 | // 35 | // If the prefix contains a fully-specified scheme component, it is considered safe only if 36 | // it starts with an allowed scheme. See safehtml.URLSanitized for more details. 37 | // 38 | // Otherwise, the prefix is safe only if it contains '/', '?', or '#', since the presence of any 39 | // of these runes ensures that this prefix, when combined with some arbitrary suffix, cannot be 40 | // interpreted as a part of a scheme. 41 | func validateURLPrefix(prefix string) error { 42 | decoded, err := decodeURLPrefix(prefix) 43 | if err != nil { 44 | return err 45 | } 46 | switch { 47 | case startsWithFullySpecifiedSchemePattern.MatchString(decoded): 48 | if safehtml.URLSanitized(decoded).String() != decoded { 49 | return fmt.Errorf("URL prefix %q contains an unsafe scheme", prefix) 50 | } 51 | case !strings.ContainsAny(decoded, "/?#"): 52 | // If the URL prefix does not already have a ':' scheme delimiter, and does not contain 53 | // '/', '?', or '#', any ':' following this prefix will be intepreted as a scheme 54 | // delimiter, causing this URL prefix to be interpreted as being part of a scheme. 55 | // e.g. `
` 56 | return fmt.Errorf("URL prefix %q is unsafe; it might be interpreted as part of a scheme", prefix) 57 | } 58 | return nil 59 | } 60 | 61 | // validateTrustedResourceURLPrefix validates if the given non-empty prefix is a safe 62 | // safehtml.TrustedResourceURL prefix. 63 | // 64 | // Prefixes are considered unsafe if they end in an incomplete HTML character reference 65 | // or percent-encoding character triplet. 66 | // 67 | // See safehtmlutil.IsSafeTrustedResourceURLPrefix for details on how the prefix is validated. 68 | func validateTrustedResourceURLPrefix(prefix string) error { 69 | decoded, err := decodeURLPrefix(prefix) 70 | if err != nil { 71 | return err 72 | } 73 | if !safehtmlutil.IsSafeTrustedResourceURLPrefix(decoded) { 74 | return fmt.Errorf("%q is a disallowed TrustedResourceURL prefix", prefix) 75 | } 76 | return nil 77 | } 78 | 79 | // endsWithPercentEncodingPrefixPattern matches strings that end in an incomplete 80 | // URL percent encoding triplet. 81 | // 82 | // See https://tools.ietf.org/html/rfc3986#section-2.1. 83 | var endsWithPercentEncodingPrefixPattern = regexp.MustCompile( 84 | `%[[:xdigit:]]?$`) 85 | 86 | // containsWhitespaceOrControlPattern matches strings that contain ASCII whitespace 87 | // or control characters. 88 | var containsWhitespaceOrControlPattern = regexp.MustCompile(`[[:space:]]|[[:cntrl:]]`) 89 | 90 | // decodeURLPrefix returns the given prefix after it has been HTML-unescaped. 91 | // It returns an error if the prefix: 92 | // - ends in an incomplete HTML character reference before HTML-unescaping, 93 | // - ends in an incomplete percent-encoding character triplet after HTML-unescaping, or 94 | // - contains whitespace before or after HTML-unescaping. 95 | func decodeURLPrefix(prefix string) (string, error) { 96 | if containsWhitespaceOrControlPattern.MatchString(prefix) { 97 | return "", fmt.Errorf("URL prefix %q contains whitespace or control characters", prefix) 98 | } 99 | if err := validateDoesNotEndsWithCharRefPrefix(prefix); err != nil { 100 | return "", fmt.Errorf("URL %s", err) 101 | } 102 | decoded := html.UnescapeString(prefix) 103 | // Check again for whitespace that might have previously been masked by a HTML reference, 104 | // such as in "javascript ". 105 | if containsWhitespaceOrControlPattern.MatchString(decoded) { 106 | return "", fmt.Errorf("URL prefix %q contains whitespace or control characters", prefix) 107 | } 108 | if endsWithPercentEncodingPrefixPattern.MatchString(decoded) { 109 | return "", fmt.Errorf("URL prefix %q ends with an incomplete percent-encoding character triplet", prefix) 110 | } 111 | return decoded, nil 112 | } 113 | 114 | func validateTrustedResourceURLSubstitution(args ...interface{}) (string, error) { 115 | input := safehtmlutil.Stringify(args...) 116 | if safehtmlutil.URLContainsDoubleDotSegment(input) { 117 | // Reject substitutions containing the ".." dot-segment to prevent the final TrustedResourceURL from referencing 118 | // a resource higher up in the path name hierarchy than the path specified in the prefix. 119 | return "", fmt.Errorf(`cannot substitute %q after TrustedResourceURL prefix: ".." is disallowed`, input) 120 | } 121 | return input, nil 122 | } 123 | -------------------------------------------------------------------------------- /template/url_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package template 6 | 7 | import ( 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestValidateURLPrefix(t *testing.T) { 13 | for _, test := range [...]struct { 14 | in string 15 | valid bool 16 | }{ 17 | // Allowed schemes or MIME types. 18 | {`http:`, true}, 19 | {`http://www.foo.com/`, true}, 20 | {`https://www.foo.com/`, true}, 21 | {`mailto://foo@foo.com.com/`, true}, 22 | {`ftp://foo.com/`, true}, 23 | {`data:image/png;base64,abc`, true}, 24 | {`data:video/mpeg;base64,abc`, true}, 25 | {`data:audio/ogg;base64,abc`, true}, 26 | {`data:image/png,abc`, true}, 27 | {`data:text/html;base64,abc`, true}, 28 | {`tel:+1-234-567-8901`, true}, 29 | // Leading and trailing newlines. 30 | {"\nhttp:", false}, 31 | {"http:\n", false}, 32 | // Disallowed schemes or MIME types. 33 | {`javascript:foo()`, false}, 34 | // No scheme, but not a scheme prefix. 35 | {`//www.foo.com/`, true}, 36 | {`/path`, true}, 37 | {`/path/x`, true}, 38 | {`/path#x`, true}, 39 | {`/path?x`, true}, 40 | {`?q=`, true}, 41 | // Scheme prefix. 42 | {`j`, false}, 43 | {`java`, false}, 44 | {`on`, false}, 45 | {`data-`, false}, 46 | // Unsafe scheme 47 | {`javascript:`, false}, 48 | {`javascript:alert`, false}, 49 | // Ends with incomplete HTML character reference. 50 | {`https&colon`, false}, 51 | // Ends with valid HTML character reference, which forms a safe prefix 52 | // after HTML-unescaping. 53 | {`https:`, true}, 54 | {`?q=`, true}, 55 | // Ends with valid HTML character reference, but forms an 56 | // unsafe scheme after HTML-unescaping. 57 | {`javascript:`, false}, 58 | } { 59 | err := validateURLPrefix(test.in) 60 | if err != nil && test.valid { 61 | t.Errorf("validateURLPrefix(%q) failed: %s", test.in, err) 62 | } else if err == nil && !test.valid { 63 | t.Errorf("validateURLPrefix(%q) succeeded unexpectedly", test.in) 64 | } 65 | } 66 | } 67 | 68 | func TestValidateTrustedResourceURLPrefix(t *testing.T) { 69 | for _, test := range [...]struct { 70 | in string 71 | valid bool 72 | }{ 73 | // Basic test cases for clearly safe and unsafe prefixes. Comprehensive test cases can 74 | // be found in TestIsSafeTrustedResourceURLPrefix in package safehtml/internal/safehtmlutil. 75 | {`https://www.foo.com/`, true}, 76 | {`javascript:alert`, false}, 77 | // Leading and trailing newlines. 78 | {"\n/path", false}, 79 | {"/path\n", false}, 80 | // Ends with incomplete HTML character reference. 81 | {`https://www.foo.com&sol`, false}, 82 | // Ends with valid HTML character reference, which forms a safe prefix 83 | // after HTML-unescaping. 84 | {`https://www.foo.com/`, true}, 85 | {`//www.foo.com/`, true}, 86 | // Ends with valid HTML character reference, but forms an 87 | // unsafe scheme after HTML-unescaping. 88 | {`javascript:`, false}, 89 | } { 90 | err := validateTrustedResourceURLPrefix(test.in) 91 | if err != nil && test.valid { 92 | t.Errorf("validateTrustedResourceURLPrefix(%q) failed: %s", test.in, err) 93 | } else if err == nil && !test.valid { 94 | t.Errorf("validateTrustedResourceURLPrefix(%q) succeeded unexpectedly", test.in) 95 | } 96 | } 97 | } 98 | 99 | func TestDecodeURLPrefix(t *testing.T) { 100 | const containsWhitespaceMsg = ` contains whitespace or control characters` 101 | const incompleteCharRefMsg = ` ends with an incomplete HTML character reference; did you mean "&" instead of "&"?` 102 | const incopletePercentEncodingMsg = ` ends with an incomplete percent-encoding character triplet` 103 | for _, test := range [...]struct { 104 | in, want, err string 105 | }{ 106 | // Contains whitespace or control characters. 107 | {" ", ``, containsWhitespaceMsg}, 108 | {"\0000", ``, containsWhitespaceMsg}, 109 | {" javascript", ``, containsWhitespaceMsg}, 110 | {"javascript\t8", ``, containsWhitespaceMsg}, 111 | {"java\nscript:", ``, containsWhitespaceMsg}, 112 | {"java script:", ``, containsWhitespaceMsg}, 113 | {"javascript\n8", ``, containsWhitespaceMsg}, 114 | {"javascript\f8", ``, containsWhitespaceMsg}, 115 | {"javascript\r8", ``, containsWhitespaceMsg}, 116 | {"javascript 8", ``, containsWhitespaceMsg}, 117 | {"javascript 8", ``, containsWhitespaceMsg}, 118 | {"javascript\00008", ``, containsWhitespaceMsg}, 119 | // Incomplete HTML character escape sequences. 120 | {`https://www.foo.com?q=bar&`, ``, incompleteCharRefMsg}, 121 | {`javascript&colon`, ``, incompleteCharRefMsg}, 122 | // Complete HTML character references. 123 | {`javascript:`, `javascript:`, ``}, 124 | {`javascript:`, `javascript:`, ``}, 125 | // Invalid URL-encoding sequences. 126 | {`/fo%`, ``, incopletePercentEncodingMsg}, 127 | {`/fo%6`, ``, incopletePercentEncodingMsg}, 128 | {`/fo%6f`, `/fo%6f`, ``}, 129 | {`/fo%6F`, `/fo%6F`, ``}, 130 | // Only HTML-unescaping, not URL-unescaping, takes place. 131 | {`foo%3a`, `foo%3a`, ``}, 132 | {`foo%3A`, `foo%3A`, ``}, 133 | } { 134 | decoded, err := decodeURLPrefix(test.in) 135 | switch { 136 | case test.err != "": 137 | if err == nil { 138 | t.Errorf("url prefix %s : expected error", test.in) 139 | } else if got := err.Error(); !strings.Contains(got, test.err) { 140 | t.Errorf("url prefix %s : error\n\t%s\ndoes not contain expected string\n\t%s", test.in, got, test.err) 141 | } 142 | case test.want != "": 143 | if got := decoded; got != test.want { 144 | t.Errorf("url prefix %s : got decoded = %s, want %s", test.in, got, test.want) 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /testconversions/testconversions.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | // Package testconversions provides functions to to create arbitrary values of 8 | // package safehtml types for use by tests only. Note that the created values may 9 | // violate type contracts. 10 | // 11 | // These functions are useful when types are constructed in a manner where using 12 | // the package safehtml API is too inconvenient. Please use the package safehtml 13 | // API whenever possible; there is value in having tests reflect common usage. 14 | // Using the package safehtml API also avoids, by design, non-contract complying 15 | // instances from being created. 16 | package testconversions 17 | 18 | import ( 19 | "github.com/google/safehtml/internal/raw" 20 | "github.com/google/safehtml" 21 | ) 22 | 23 | var html = raw.HTML.(func(string) safehtml.HTML) 24 | var script = raw.Script.(func(string) safehtml.Script) 25 | var style = raw.Style.(func(string) safehtml.Style) 26 | var styleSheet = raw.StyleSheet.(func(string) safehtml.StyleSheet) 27 | var url = raw.URL.(func(string) safehtml.URL) 28 | var trustedResourceURL = raw.TrustedResourceURL.(func(string) safehtml.TrustedResourceURL) 29 | var identifier = raw.Identifier.(func(string) safehtml.Identifier) 30 | 31 | // MakeHTMLForTest converts a plain string into a HTML. 32 | // This function must only be used in tests. 33 | func MakeHTMLForTest(s string) safehtml.HTML { 34 | return html(s) 35 | } 36 | 37 | // MakeScriptForTest converts a plain string into a Script. 38 | // This function must only be used in tests. 39 | func MakeScriptForTest(s string) safehtml.Script { 40 | return script(s) 41 | } 42 | 43 | // MakeStyleForTest converts a plain string into a Style. 44 | // This function must only be used in tests. 45 | func MakeStyleForTest(s string) safehtml.Style { 46 | return style(s) 47 | } 48 | 49 | // MakeStyleSheetForTest converts a plain string into a StyleSheet. 50 | // This function must only be used in tests. 51 | func MakeStyleSheetForTest(s string) safehtml.StyleSheet { 52 | return styleSheet(s) 53 | } 54 | 55 | // MakeURLForTest converts a plain string into a URL. 56 | // This function must only be used in tests. 57 | func MakeURLForTest(s string) safehtml.URL { 58 | return url(s) 59 | } 60 | 61 | // MakeTrustedResourceURLForTest converts a plain string into a TrustedResourceURL. 62 | // This function must only be used in tests. 63 | func MakeTrustedResourceURLForTest(s string) safehtml.TrustedResourceURL { 64 | return trustedResourceURL(s) 65 | } 66 | 67 | // MakeIdentifierForTest converts a plain string into an Identifier. 68 | // This function must only be used in tests. 69 | func MakeIdentifierForTest(s string) safehtml.Identifier { 70 | return identifier(s) 71 | } 72 | -------------------------------------------------------------------------------- /trustedresourceurl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package safehtml 8 | 9 | import ( 10 | "fmt" 11 | "regexp" 12 | "sort" 13 | "strings" 14 | 15 | "flag" 16 | "github.com/google/safehtml/internal/safehtmlutil" 17 | ) 18 | 19 | // A TrustedResourceURL is an immutable string-like type referencing the 20 | // application’s own, trusted resources. It can be used to safely load scripts, 21 | // CSS and other sensitive resources without the risk of untrusted code execution. 22 | // For example, it is unsafe to insert a plain string in a 23 | // 24 | // 25 | // 26 | // context since the URL may originate from untrusted user input and the 27 | // script it is pointing to may thus be controlled by an attacker. It is, 28 | // however, safe to use a TrustedResourceURL since its value is known to never 29 | // have left application control. 30 | // 31 | // In order to ensure that an attacker cannot influence the TrustedResourceURL 32 | // value, a TrustedResourceURL can only be instantiated from compile-time 33 | // constant string literals, command-line flags or a combination of the two, 34 | // but never from arbitrary string values potentially representing untrusted user input. 35 | // 36 | // Additionally, TrustedResourceURLs can be serialized and passed along within 37 | // the application via protocol buffers. It is the application’s responsibility 38 | // to ensure that the protocol buffers originate from within the application 39 | // itself and not from an external entity outside its trust domain. 40 | // 41 | // Note that TrustedResourceURLs can also use absolute paths (starting with '/') 42 | // and relative paths. This allows the same binary to be used for different 43 | // hosts without hard-coding the hostname in a string literal. 44 | type TrustedResourceURL struct { 45 | // We declare a TrustedResourceURL not as a string but as a struct wrapping a string 46 | // to prevent construction of TrustedResourceURL values through string conversion. 47 | str string 48 | } 49 | 50 | // TrustedResourceURLWithParams constructs a new TrustedResourceURL with the 51 | // given key-value pairs added as query parameters. 52 | // 53 | // Map entries with empty keys or values are ignored. The order of appended 54 | // keys is guaranteed to be stable but may differ from the order in input. 55 | func TrustedResourceURLWithParams(t TrustedResourceURL, params map[string]string) TrustedResourceURL { 56 | url := t.str 57 | var fragment string 58 | if i := strings.IndexByte(url, '#'); i != -1 { 59 | // The fragment identifier component will always appear at the end 60 | // of the URL after the query segment. It is therefore safe to 61 | // trim the fragment from the tail of the URL and re-append it after 62 | // all query parameters have been added. 63 | // See https://tools.ietf.org/html/rfc3986#appendix-A. 64 | fragment = url[i:] 65 | url = url[:i] 66 | } 67 | sep := "?" 68 | if i := strings.IndexRune(url, '?'); i != -1 { 69 | // The first "?" in a URL indicates the start of the query component. 70 | // See https://tools.ietf.org/html/rfc3986#section-3.4 71 | if i == len(url)-1 { 72 | sep = "" 73 | } else { 74 | sep = "&" 75 | } 76 | } 77 | stringParams := make([]string, 0, len(params)) 78 | for k, v := range params { 79 | if k == "" || v == "" { 80 | continue 81 | } 82 | stringParam := safehtmlutil.QueryEscapeURL(k) + "=" + safehtmlutil.QueryEscapeURL(v) 83 | stringParams = append(stringParams, stringParam) 84 | } 85 | if len(stringParams) > 0 { 86 | sort.Strings(stringParams) 87 | url += sep + strings.Join(stringParams, "&") 88 | } 89 | return TrustedResourceURL{url + fragment} 90 | } 91 | 92 | // TrustedResourceURLFromConstant constructs a TrustedResourceURL with its underlying 93 | // URL set to the given url, which must be an untyped string constant. 94 | // 95 | // No runtime validation or sanitization is performed on url; being under 96 | // application control, it is simply assumed to comply with the TrustedResourceURL type 97 | // contract. 98 | func TrustedResourceURLFromConstant(url stringConstant) TrustedResourceURL { 99 | return TrustedResourceURL{string(url)} 100 | } 101 | 102 | // TrustedResourceURLFormatFromConstant constructs a TrustedResourceURL from a 103 | // format string, which must be an untyped string constant, and string arguments. 104 | // 105 | // Arguments are specified as a map of labels, which must contain only alphanumeric 106 | // and '_' runes, to string values. Each `%{