aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/google/safehtml/trustedresourceurl.go
blob: e31a2fd566e6a02ec6c161ffdb706228bd102485 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Copyright (c) 2017 The Go Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

package safehtml

import (
	"fmt"
	"regexp"
	"sort"
	"strings"

	"flag"
	"github.com/google/safehtml/internal/safehtmlutil"
)

// A TrustedResourceURL is an immutable string-like type referencing the
// application’s own, trusted resources. It can be used to safely load scripts,
// CSS and other sensitive resources without the risk of untrusted code execution.
// For example, it is unsafe to insert a plain string in a
//
//     <script src=“...”></script>
//
// context since the URL may originate from untrusted user input and the
// script it is pointing to may thus be controlled by an attacker. It is,
// however, safe to use a TrustedResourceURL since its value is known to never
// have left application control.
//
// In order to ensure that an attacker cannot influence the TrustedResourceURL
// value, a TrustedResourceURL can only be instantiated from compile-time
// constant string literals, command-line flags or a combination of the two,
// but never from arbitrary string values potentially representing untrusted user input.
//
// Additionally, TrustedResourceURLs can be serialized and passed along within
// the application via protocol buffers. It is the application’s responsibility
// to ensure that the protocol buffers originate from within the application
// itself and not from an external entity outside its trust domain.
//
// Note that TrustedResourceURLs can also use absolute paths (starting with '/')
// and relative paths. This allows the same binary to be used for different
// hosts without hard-coding the hostname in a string literal.
type TrustedResourceURL struct {
	// We declare a TrustedResourceURL not as a string but as a struct wrapping a string
	// to prevent construction of TrustedResourceURL values through string conversion.
	str string
}

// TrustedResourceURLWithParams constructs a new TrustedResourceURL with the
// given key-value pairs added as query parameters.
//
// Map entries with empty keys or values are ignored. The order of appended
// keys is guaranteed to be stable but may differ from the order in input.
func TrustedResourceURLWithParams(t TrustedResourceURL, params map[string]string) TrustedResourceURL {
	url := t.str
	var fragment string
	if i := strings.IndexByte(url, '#'); i != -1 {
		// The fragment identifier component will always appear at the end
		// of the URL after the query segment. It is therefore safe to
		// trim the fragment from the tail of the URL and re-append it after
		// all query parameters have been added.
		// See https://tools.ietf.org/html/rfc3986#appendix-A.
		fragment = url[i:]
		url = url[:i]
	}
	sep := "?"
	if i := strings.IndexRune(url, '?'); i != -1 {
		// The first "?" in a URL indicates the start of the query component.
		// See https://tools.ietf.org/html/rfc3986#section-3.4
		if i == len(url)-1 {
			sep = ""
		} else {
			sep = "&"
		}
	}
	stringParams := make([]string, 0, len(params))
	for k, v := range params {
		if k == "" || v == "" {
			continue
		}
		stringParam := safehtmlutil.QueryEscapeURL(k) + "=" + safehtmlutil.QueryEscapeURL(v)
		stringParams = append(stringParams, stringParam)
	}
	if len(stringParams) > 0 {
		sort.Strings(stringParams)
		url += sep + strings.Join(stringParams, "&")
	}
	return TrustedResourceURL{url + fragment}
}

// TrustedResourceURLFromConstant constructs a TrustedResourceURL with its underlying
// URL set to the given url, which must be an untyped string constant.
//
// No runtime validation or sanitization is performed on url; being under
// application control, it is simply assumed to comply with the TrustedResourceURL type
// contract.
func TrustedResourceURLFromConstant(url stringConstant) TrustedResourceURL {
	return TrustedResourceURL{string(url)}
}

// TrustedResourceURLFormatFromConstant constructs a TrustedResourceURL from a
// format string, which must be an untyped string constant, and string arguments.
//
// Arguments are specified as a map of labels, which must contain only alphanumeric
// and '_' runes, to string values. Each `%{<label>}` marker in the format string is
// replaced by the string value identified by <label> after it has been URL-escaped.
// Arguments that do not match any label in the format string are ignored.
//
// The format string must have a prefix of one of the following forms:
//    * `https://<origin>/`
//    * `//<origin>/`
//    * `/<pathStart>`
//    * `about:blank#`
//
// `<origin>` must contain only alphanumerics, '.', ':', '[', ']', or '-', and
// `<pathStart>` is any character except `/` and `\`.
func TrustedResourceURLFormatFromConstant(format stringConstant, args map[string]string) (TrustedResourceURL, error) {
	return trustedResourceURLFormat(string(format), args)
}

// TrustedResourceURLFormatFromFlag is a variant of TrustedResourceURLFormatFromConstant
// that constructs a TrustedResourceURL from a format string, which is given as a flag.Value,
// and string arguments.
//
// See TrustedResourceURLFormatFromConstant for more details about format
// string markers and validation.
func TrustedResourceURLFormatFromFlag(format flag.Value, args map[string]string) (TrustedResourceURL, error) {
	return trustedResourceURLFormat(fmt.Sprint(format.String()), args)
}

func trustedResourceURLFormat(format string, args map[string]string) (TrustedResourceURL, error) {
	if !safehtmlutil.IsSafeTrustedResourceURLPrefix(format) {
		return TrustedResourceURL{}, fmt.Errorf("%q is a disallowed TrustedResourceURL format string", format)
	}
	var err error
	ret := trustedResourceURLFormatMarkerPattern.ReplaceAllStringFunc(format, func(match string) string {
		argName := match[len("%{") : len(match)-len("}")]
		argVal, ok := args[argName]
		if !ok {
			if err == nil {
				// Report an error for the first missing argument.
				err = fmt.Errorf("expected argument named %q", argName)
			}
			return ""
		}
		if safehtmlutil.URLContainsDoubleDotSegment(argVal) {
			// Reject values containing the ".." dot-segment to prevent the final TrustedResourceURL from referencing
			// a resource higher up in the path name hierarchy than the path specified in the prefix.
			err = fmt.Errorf(`argument %q with value %q must not contain ".."`, argName, argVal)
			return ""
		}
		// QueryEscapeURL escapes some non-reserved characters in the path
		// segment (e.g. '/' and '?') in order to prevent the injection of any new path
		// segments or URL components.
		return safehtmlutil.QueryEscapeURL(argVal)
	})
	return TrustedResourceURL{ret}, err
}

// trustedResourceURLFormatMarkerPattern matches markers in TrustedResourceURLFormat
// format strings.
var trustedResourceURLFormatMarkerPattern = regexp.MustCompile(`%{[[:word:]]+}`)

// TrustedResourceURLFromFlag returns a TrustedResourceURL containing the string
// representation of the retrieved value of the flag.
//
// In a server setting, flags are part of the application's deployment
// configuration and are hence considered application-controlled.
func TrustedResourceURLFromFlag(value flag.Value) TrustedResourceURL {
	return TrustedResourceURL{fmt.Sprint(value.String())}
}

// String returns the string form of the TrustedResourceURL.
func (t TrustedResourceURL) String() string {
	return t.str
}

// TrustedResourceURLAppend URL-escapes a string and appends it to the TrustedResourceURL.
//
// This function can only be used if the TrustedResourceURL has a prefix of one of the following
// forms:
//    * `https://<origin>/`
//    * `//<origin>/`
//    * `/<pathStart>`
//    * `about:blank#`
//
// `<origin>` must contain only alphanumerics, '.', ':', '[', ']', or '-', and
// `<pathStart>` is any character except `/` and `\`.
func TrustedResourceURLAppend(t TrustedResourceURL, s string) (TrustedResourceURL, error) {
	if !safehtmlutil.IsSafeTrustedResourceURLPrefix(t.str) {
		return TrustedResourceURL{}, fmt.Errorf("cannot append to TrustedResourceURL %q because it has an unsafe prefix", t)
	}
	return TrustedResourceURL{t.str + safehtmlutil.QueryEscapeURL(s)}, nil
}