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  }