github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/htmlinject/htmlinject_test.go (about) 1 // Copyright 2020 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package htmlinject 16 17 import ( 18 "html/template" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "strings" 23 "testing" 24 25 "github.com/google/go-cmp/cmp" 26 safetemplate "github.com/google/safehtml/template" 27 "github.com/google/safehtml/template/uncheckedconversions" 28 ) 29 30 func ExampleTransform() { 31 const in = ` 32 <html> 33 <head> 34 <link rel=preload as="script" src="gopher.js"> 35 </head> 36 <body> 37 {{.Content}} 38 <script type="application/javascript">alert("script")</script> 39 <form> 40 First name:<br> 41 <input type="text" name="firstname"><br> 42 Last name:<br> 43 <input type="text" name="lastname"> 44 </form> 45 </body> 46 </html> 47 ` 48 got, err := Transform(strings.NewReader(in), CSPNoncesDefault, XSRFTokensDefault) 49 if err != nil { 50 // handle error 51 panic(err) 52 } 53 template.Must(template.New("example transform").Funcs(map[string]interface{}{ 54 "XSRFToken": func() string { return "XSRFToken-secret" }, 55 "CSPNonce": func() string { return "CSPNonce-secret" }, 56 }).Parse(got)).Execute(os.Stdout, map[string]string{"Content": "This is some content"}) 57 // Output: 58 // <html> 59 // <head> 60 // <link nonce="CSPNonce-secret" rel=preload as="script" src="gopher.js"> 61 // </head> 62 // <body> 63 // This is some content 64 // <script nonce="CSPNonce-secret" type="application/javascript">alert("script")</script> 65 // <form><input type="hidden" name="xsrf-token" value="XSRFToken-secret"> 66 // First name:<br> 67 // <input type="text" name="firstname"><br> 68 // Last name:<br> 69 // <input type="text" name="lastname"> 70 // </form> 71 // </body> 72 // </html> 73 } 74 75 func BenchmarkTransform(b *testing.B) { 76 b.ReportAllocs() 77 var ( 78 config = []TransformConfig{CSPNoncesDefault, XSRFTokensDefault} 79 in = ` 80 <html> 81 <head> 82 <link rel=preload as="script" src="gopher.js"> 83 </head> 84 <body> 85 <script type="application/javascript">alert("script")</script> 86 <form> 87 First name:<br> 88 <input type="text" name="firstname"><br> 89 Last name:<br> 90 <input type="text" name="lastname"> 91 </form> 92 </body> 93 </html> 94 ` 95 want = ` 96 <html> 97 <head> 98 <link nonce="{{CSPNonce}}" rel=preload as="script" src="gopher.js"> 99 </head> 100 <body> 101 <script nonce="{{CSPNonce}}" type="application/javascript">alert("script")</script> 102 <form><input type="hidden" name="xsrf-token" value="{{XSRFToken}}"> 103 First name:<br> 104 <input type="text" name="firstname"><br> 105 Last name:<br> 106 <input type="text" name="lastname"> 107 </form> 108 </body> 109 </html> 110 ` 111 ) 112 b.ResetTimer() 113 for i := 0; i < b.N; i++ { 114 got, err := Transform(strings.NewReader(in), config...) 115 if err != nil { 116 b.Fatalf("Transform: got err %q, didn't want one", err) 117 } 118 if got != want { 119 b.Errorf("got %q, want %q", got, want) 120 } 121 } 122 } 123 124 var tests = []struct { 125 name string 126 xsrf, csp bool 127 in, want string 128 }{ 129 { 130 name: "nothing to change", 131 in: ` 132 <html> 133 <header><title>This is title</title></header> 134 <body> 135 Hello world 136 </body> 137 </html> 138 `, 139 want: ` 140 <html> 141 <header><title>This is title</title></header> 142 <body> 143 Hello world 144 </body> 145 </html> 146 `, 147 }, 148 { 149 name: "add CSP nonces", 150 csp: true, 151 in: ` 152 <html> 153 <head> 154 <link rel=preload as="script" src="gopher.js"> 155 <style> 156 h1 { 157 border: 5px solid yellow; 158 } 159 </style> 160 </head> 161 <body> 162 <script type="application/javascript">alert("script")</script> 163 </body> 164 </html> 165 `, 166 want: ` 167 <html> 168 <head> 169 <link nonce="{{CSPNonce}}" rel=preload as="script" src="gopher.js"> 170 <style nonce="{{CSPNonce}}"> 171 h1 { 172 border: 5px solid yellow; 173 } 174 </style> 175 </head> 176 <body> 177 <script nonce="{{CSPNonce}}" type="application/javascript">alert("script")</script> 178 </body> 179 </html> 180 `, 181 }, 182 { 183 name: "add XSRF protection", 184 xsrf: true, 185 in: ` 186 <form> 187 First name:<br> 188 <input type="text" name="firstname"><br> 189 Last name:<br> 190 <input type="text" name="lastname"> 191 </form> 192 `, 193 want: ` 194 <form><input type="hidden" name="xsrf-token" value="{{XSRFToken}}"> 195 First name:<br> 196 <input type="text" name="firstname"><br> 197 Last name:<br> 198 <input type="text" name="lastname"> 199 </form> 200 `, 201 }, 202 { 203 name: "all configs", 204 xsrf: true, 205 csp: true, 206 in: ` 207 <html> 208 <head> 209 <link rel="stylesheet" href="styles.css"> 210 <link rel=preload as="script" src="gopher.js"> 211 </head> 212 <body> 213 <script type="application/javascript">alert("script")</script> 214 <form> 215 First name:<br> 216 <input type="text" name="firstname"><br> 217 Last name:<br> 218 <input type="text" name="lastname"> 219 </form> 220 </body> 221 </html> 222 `, 223 want: ` 224 <html> 225 <head> 226 <link rel="stylesheet" href="styles.css"> 227 <link nonce="{{CSPNonce}}" rel=preload as="script" src="gopher.js"> 228 </head> 229 <body> 230 <script nonce="{{CSPNonce}}" type="application/javascript">alert("script")</script> 231 <form><input type="hidden" name="xsrf-token" value="{{XSRFToken}}"> 232 First name:<br> 233 <input type="text" name="firstname"><br> 234 Last name:<br> 235 <input type="text" name="lastname"> 236 </form> 237 </body> 238 </html> 239 `, 240 }, 241 } 242 243 func TestTransform(t *testing.T) { 244 for _, tt := range tests { 245 t.Run(tt.name, func(t *testing.T) { 246 cfg := []TransformConfig{} 247 if tt.csp { 248 cfg = append(cfg, CSPNoncesDefault) 249 } 250 if tt.xsrf { 251 cfg = append(cfg, XSRFTokensDefault) 252 253 } 254 got, err := Transform(strings.NewReader(tt.in), cfg...) 255 if err != nil { 256 t.Fatalf("Transform: got err %q, didn't want one", err) 257 } 258 if diff := cmp.Diff(tt.want, got); diff != "" { 259 t.Errorf("-want +got %s", diff) 260 } 261 }) 262 } 263 } 264 265 func TestLoadTrustedTemplateWithDefaultConfig(t *testing.T) { 266 for _, tt := range tests { 267 t.Run(tt.name, func(t *testing.T) { 268 cfg := LoadConfig{DisableCSP: !tt.csp, DisableXSRF: !tt.xsrf} 269 gotTpl, err := LoadTrustedTemplate(nil, cfg, uncheckedconversions.TrustedTemplateFromStringKnownToSatisfyTypeContract(tt.in)) 270 if err != nil { 271 t.Fatalf("LoadTrustedTemplate: got err %q", err) 272 } 273 // Test that whatever we provide is clonable or injection won't be possible. 274 gotTpl, err = gotTpl.Clone() 275 if err != nil { 276 t.Fatalf("Clone loaded template: got err %q", err) 277 } 278 var sb strings.Builder 279 // Make functions return the source code that calls them and compare the results with the source. 280 // Sadly there is no good way to take the parsed sources out of a text/template.Template. 281 err = gotTpl.Funcs(map[string]interface{}{ 282 XSRFTokensDefaultFuncName: func() string { return "{{" + XSRFTokensDefaultFuncName + "}}" }, 283 CSPNoncesDefaultFuncName: func() string { return "{{" + CSPNoncesDefaultFuncName + "}}" }, 284 }).Execute(&sb, nil) 285 if err != nil { 286 t.Fatalf("Execute: got err %q", err) 287 } 288 if diff := cmp.Diff(tt.want, sb.String()); diff != "" { 289 t.Errorf("-want +got %s", diff) 290 } 291 }) 292 } 293 } 294 295 func TestLoadGlob(t *testing.T) { 296 tpl, err := LoadGlob(nil, LoadConfig{}, safetemplate.TrustedSourceFromConstant("testdata/*.tpl")) 297 298 // Test that whatever we provide is clonable or injection won't be possible. 299 tpl, err = tpl.Clone() 300 if err != nil { 301 t.Fatalf("Clone loaded template: got err %q", err) 302 } 303 tpl = tpl.Funcs(map[string]interface{}{ 304 XSRFTokensDefaultFuncName: func() string { return "{{" + XSRFTokensDefaultFuncName + "}}" }, 305 CSPNoncesDefaultFuncName: func() string { return "{{" + CSPNoncesDefaultFuncName + "}}" }, 306 }) 307 if got, want := len(tpl.Templates()), 2; got != want { 308 t.Fatalf("Loaded templates: got %d want %d %s", got, want, tpl.DefinedTemplates()) 309 } 310 for _, inner := range tpl.Templates() { 311 t.Run(inner.Name(), func(t *testing.T) { 312 var sb strings.Builder 313 err := tpl.ExecuteTemplate(&sb, inner.Name(), nil) 314 if err != nil { 315 t.Fatalf("Executing: %v", err) 316 } 317 stripExt := strings.TrimSuffix(inner.Name(), filepath.Ext(inner.Name())) 318 b, err := ioutil.ReadFile("testdata/" + stripExt + ".want") 319 if err != nil { 320 t.Fatalf("Reading '.want' file: %v", err) 321 } 322 if got, want := sb.String(), string(b); got != want { 323 t.Errorf("got: %v, want: %v", got, want) 324 } 325 }) 326 } 327 }