...

Source file src/html/template/url.go

Documentation: html/template

		 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  	"strings"
		11  )
		12  
		13  // urlFilter returns its input unless it contains an unsafe scheme in which
		14  // case it defangs the entire URL.
		15  //
		16  // Schemes that cause unintended side effects that are irreversible without user
		17  // interaction are considered unsafe. For example, clicking on a "javascript:"
		18  // link can immediately trigger JavaScript code execution.
		19  //
		20  // This filter conservatively assumes that all schemes other than the following
		21  // are unsafe:
		22  //		* http:	 Navigates to a new website, and may open a new window or tab.
		23  //							These side effects can be reversed by navigating back to the
		24  //							previous website, or closing the window or tab. No irreversible
		25  //							changes will take place without further user interaction with
		26  //							the new website.
		27  //		* https:	Same as http.
		28  //		* mailto: Opens an email program and starts a new draft. This side effect
		29  //							is not irreversible until the user explicitly clicks send; it
		30  //							can be undone by closing the email program.
		31  //
		32  // To allow URLs containing other schemes to bypass this filter, developers must
		33  // explicitly indicate that such a URL is expected and safe by encapsulating it
		34  // in a template.URL value.
		35  func urlFilter(args ...interface{}) string {
		36  	s, t := stringify(args...)
		37  	if t == contentTypeURL {
		38  		return s
		39  	}
		40  	if !isSafeURL(s) {
		41  		return "#" + filterFailsafe
		42  	}
		43  	return s
		44  }
		45  
		46  // isSafeURL is true if s is a relative URL or if URL has a protocol in
		47  // (http, https, mailto).
		48  func isSafeURL(s string) bool {
		49  	if i := strings.IndexRune(s, ':'); i >= 0 && !strings.ContainsRune(s[:i], '/') {
		50  
		51  		protocol := s[:i]
		52  		if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") {
		53  			return false
		54  		}
		55  	}
		56  	return true
		57  }
		58  
		59  // urlEscaper produces an output that can be embedded in a URL query.
		60  // The output can be embedded in an HTML attribute without further escaping.
		61  func urlEscaper(args ...interface{}) string {
		62  	return urlProcessor(false, args...)
		63  }
		64  
		65  // urlNormalizer normalizes URL content so it can be embedded in a quote-delimited
		66  // string or parenthesis delimited url(...).
		67  // The normalizer does not encode all HTML specials. Specifically, it does not
		68  // encode '&' so correct embedding in an HTML attribute requires escaping of
		69  // '&' to '&'.
		70  func urlNormalizer(args ...interface{}) string {
		71  	return urlProcessor(true, args...)
		72  }
		73  
		74  // urlProcessor normalizes (when norm is true) or escapes its input to produce
		75  // a valid hierarchical or opaque URL part.
		76  func urlProcessor(norm bool, args ...interface{}) string {
		77  	s, t := stringify(args...)
		78  	if t == contentTypeURL {
		79  		norm = true
		80  	}
		81  	var b bytes.Buffer
		82  	if processURLOnto(s, norm, &b) {
		83  		return b.String()
		84  	}
		85  	return s
		86  }
		87  
		88  // processURLOnto appends a normalized URL corresponding to its input to b
		89  // and reports whether the appended content differs from s.
		90  func processURLOnto(s string, norm bool, b *bytes.Buffer) bool {
		91  	b.Grow(len(s) + 16)
		92  	written := 0
		93  	// The byte loop below assumes that all URLs use UTF-8 as the
		94  	// content-encoding. This is similar to the URI to IRI encoding scheme
		95  	// defined in section 3.1 of	RFC 3987, and behaves the same as the
		96  	// EcmaScript builtin encodeURIComponent.
		97  	// It should not cause any misencoding of URLs in pages with
		98  	// Content-type: text/html;charset=UTF-8.
		99  	for i, n := 0, len(s); i < n; i++ {
	 100  		c := s[i]
	 101  		switch c {
	 102  		// Single quote and parens are sub-delims in RFC 3986, but we
	 103  		// escape them so the output can be embedded in single
	 104  		// quoted attributes and unquoted CSS url(...) constructs.
	 105  		// Single quotes are reserved in URLs, but are only used in
	 106  		// the obsolete "mark" rule in an appendix in RFC 3986
	 107  		// so can be safely encoded.
	 108  		case '!', '#', '$', '&', '*', '+', ',', '/', ':', ';', '=', '?', '@', '[', ']':
	 109  			if norm {
	 110  				continue
	 111  			}
	 112  		// Unreserved according to RFC 3986 sec 2.3
	 113  		// "For consistency, percent-encoded octets in the ranges of
	 114  		// ALPHA (%41-%5A and %61-%7A), DIGIT (%30-%39), hyphen (%2D),
	 115  		// period (%2E), underscore (%5F), or tilde (%7E) should not be
	 116  		// created by URI producers
	 117  		case '-', '.', '_', '~':
	 118  			continue
	 119  		case '%':
	 120  			// When normalizing do not re-encode valid escapes.
	 121  			if norm && i+2 < len(s) && isHex(s[i+1]) && isHex(s[i+2]) {
	 122  				continue
	 123  			}
	 124  		default:
	 125  			// Unreserved according to RFC 3986 sec 2.3
	 126  			if 'a' <= c && c <= 'z' {
	 127  				continue
	 128  			}
	 129  			if 'A' <= c && c <= 'Z' {
	 130  				continue
	 131  			}
	 132  			if '0' <= c && c <= '9' {
	 133  				continue
	 134  			}
	 135  		}
	 136  		b.WriteString(s[written:i])
	 137  		fmt.Fprintf(b, "%%%02x", c)
	 138  		written = i + 1
	 139  	}
	 140  	b.WriteString(s[written:])
	 141  	return written != 0
	 142  }
	 143  
	 144  // Filters and normalizes srcset values which are comma separated
	 145  // URLs followed by metadata.
	 146  func srcsetFilterAndEscaper(args ...interface{}) string {
	 147  	s, t := stringify(args...)
	 148  	switch t {
	 149  	case contentTypeSrcset:
	 150  		return s
	 151  	case contentTypeURL:
	 152  		// Normalizing gets rid of all HTML whitespace
	 153  		// which separate the image URL from its metadata.
	 154  		var b bytes.Buffer
	 155  		if processURLOnto(s, true, &b) {
	 156  			s = b.String()
	 157  		}
	 158  		// Additionally, commas separate one source from another.
	 159  		return strings.ReplaceAll(s, ",", "%2c")
	 160  	}
	 161  
	 162  	var b bytes.Buffer
	 163  	written := 0
	 164  	for i := 0; i < len(s); i++ {
	 165  		if s[i] == ',' {
	 166  			filterSrcsetElement(s, written, i, &b)
	 167  			b.WriteString(",")
	 168  			written = i + 1
	 169  		}
	 170  	}
	 171  	filterSrcsetElement(s, written, len(s), &b)
	 172  	return b.String()
	 173  }
	 174  
	 175  // Derived from https://play.golang.org/p/Dhmj7FORT5
	 176  const htmlSpaceAndASCIIAlnumBytes = "\x00\x36\x00\x00\x01\x00\xff\x03\xfe\xff\xff\x07\xfe\xff\xff\x07"
	 177  
	 178  // isHTMLSpace is true iff c is a whitespace character per
	 179  // https://infra.spec.whatwg.org/#ascii-whitespace
	 180  func isHTMLSpace(c byte) bool {
	 181  	return (c <= 0x20) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
	 182  }
	 183  
	 184  func isHTMLSpaceOrASCIIAlnum(c byte) bool {
	 185  	return (c < 0x80) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
	 186  }
	 187  
	 188  func filterSrcsetElement(s string, left int, right int, b *bytes.Buffer) {
	 189  	start := left
	 190  	for start < right && isHTMLSpace(s[start]) {
	 191  		start++
	 192  	}
	 193  	end := right
	 194  	for i := start; i < right; i++ {
	 195  		if isHTMLSpace(s[i]) {
	 196  			end = i
	 197  			break
	 198  		}
	 199  	}
	 200  	if url := s[start:end]; isSafeURL(url) {
	 201  		// If image metadata is only spaces or alnums then
	 202  		// we don't need to URL normalize it.
	 203  		metadataOk := true
	 204  		for i := end; i < right; i++ {
	 205  			if !isHTMLSpaceOrASCIIAlnum(s[i]) {
	 206  				metadataOk = false
	 207  				break
	 208  			}
	 209  		}
	 210  		if metadataOk {
	 211  			b.WriteString(s[left:start])
	 212  			processURLOnto(url, true, b)
	 213  			b.WriteString(s[end:right])
	 214  			return
	 215  		}
	 216  	}
	 217  	b.WriteString("#")
	 218  	b.WriteString(filterFailsafe)
	 219  }
	 220  

View as plain text