Skip to content

Commit 5c48951

Browse files
Roberto ClapisFiloSottile
Roberto Clapis
authored andcommitted
net/http: switch HTTP1 to ASCII equivalents of string functions
The current implementation uses UTF-aware functions like strings.EqualFold and strings.ToLower. This could, in some cases, cause http smuggling. Change-Id: I0e76a993470a1e1b1b472f4b2859ea0a2b22ada0 Reviewed-on: https://go-review.googlesource.com/c/go/+/308009 Run-TryBot: Filippo Valsorda <[email protected]> TryBot-Result: Go Bot <[email protected]> Trust: Roberto Clapis <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]>
1 parent dc50683 commit 5c48951

File tree

14 files changed

+215
-38
lines changed

14 files changed

+215
-38
lines changed

src/go/build/deps_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ var depsRules = `
440440
# HTTP, King of Dependencies.
441441
442442
FMT
443-
< golang.org/x/net/http2/hpack, net/http/internal;
443+
< golang.org/x/net/http2/hpack, net/http/internal, net/http/internal/ascii;
444444
445445
FMT, NET, container/list, encoding/binary, log
446446
< golang.org/x/text/transform
@@ -458,6 +458,7 @@ var depsRules = `
458458
golang.org/x/net/http/httpproxy,
459459
golang.org/x/net/http2/hpack,
460460
net/http/internal,
461+
net/http/internal/ascii,
461462
net/http/httptrace,
462463
mime/multipart,
463464
log
@@ -468,7 +469,7 @@ var depsRules = `
468469
encoding/json, net/http
469470
< expvar;
470471
471-
net/http
472+
net/http, net/http/internal/ascii
472473
< net/http/cookiejar, net/http/httputil;
473474
474475
net/http, flag

src/net/http/client.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"fmt"
1818
"io"
1919
"log"
20+
"net/http/internal/ascii"
2021
"net/url"
2122
"reflect"
2223
"sort"
@@ -547,7 +548,10 @@ func urlErrorOp(method string) string {
547548
if method == "" {
548549
return "Get"
549550
}
550-
return method[:1] + strings.ToLower(method[1:])
551+
if lowerMethod, ok := ascii.ToLower(method); ok {
552+
return method[:1] + lowerMethod[1:]
553+
}
554+
return method
551555
}
552556

553557
// Do sends an HTTP request and returns an HTTP response, following

src/net/http/cookie.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package http
77
import (
88
"log"
99
"net"
10+
"net/http/internal/ascii"
1011
"net/textproto"
1112
"strconv"
1213
"strings"
@@ -93,15 +94,23 @@ func readSetCookies(h Header) []*Cookie {
9394
if j := strings.Index(attr, "="); j >= 0 {
9495
attr, val = attr[:j], attr[j+1:]
9596
}
96-
lowerAttr := strings.ToLower(attr)
97+
lowerAttr, isASCII := ascii.ToLower(attr)
98+
if !isASCII {
99+
continue
100+
}
97101
val, ok = parseCookieValue(val, false)
98102
if !ok {
99103
c.Unparsed = append(c.Unparsed, parts[i])
100104
continue
101105
}
106+
102107
switch lowerAttr {
103108
case "samesite":
104-
lowerVal := strings.ToLower(val)
109+
lowerVal, ascii := ascii.ToLower(val)
110+
if !ascii {
111+
c.SameSite = SameSiteDefaultMode
112+
continue
113+
}
105114
switch lowerVal {
106115
case "lax":
107116
c.SameSite = SameSiteLaxMode

src/net/http/cookiejar/jar.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"net"
1212
"net/http"
13+
"net/http/internal/ascii"
1314
"net/url"
1415
"sort"
1516
"strings"
@@ -296,7 +297,6 @@ func (j *Jar) setCookies(u *url.URL, cookies []*http.Cookie, now time.Time) {
296297
// host name.
297298
func canonicalHost(host string) (string, error) {
298299
var err error
299-
host = strings.ToLower(host)
300300
if hasPort(host) {
301301
host, _, err = net.SplitHostPort(host)
302302
if err != nil {
@@ -307,7 +307,13 @@ func canonicalHost(host string) (string, error) {
307307
// Strip trailing dot from fully qualified domain names.
308308
host = host[:len(host)-1]
309309
}
310-
return toASCII(host)
310+
encoded, err := toASCII(host)
311+
if err != nil {
312+
return "", err
313+
}
314+
// We know this is ascii, no need to check.
315+
lower, _ := ascii.ToLower(encoded)
316+
return lower, nil
311317
}
312318

313319
// hasPort reports whether host contains a port number. host may be a host
@@ -469,7 +475,12 @@ func (j *Jar) domainAndType(host, domain string) (string, bool, error) {
469475
// both are illegal.
470476
return "", false, errMalformedDomain
471477
}
472-
domain = strings.ToLower(domain)
478+
479+
domain, isASCII := ascii.ToLower(domain)
480+
if !isASCII {
481+
// Received non-ASCII domain, e.g. "perché.com" instead of "xn--perch-fsa.com"
482+
return "", false, errMalformedDomain
483+
}
473484

474485
if domain[len(domain)-1] == '.' {
475486
// We received stuff like "Domain=www.example.com.".

src/net/http/cookiejar/punycode.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package cookiejar
88

99
import (
1010
"fmt"
11+
"net/http/internal/ascii"
1112
"strings"
1213
"unicode/utf8"
1314
)
@@ -133,12 +134,12 @@ const acePrefix = "xn--"
133134
// toASCII("bücher.example.com") is "xn--bcher-kva.example.com", and
134135
// toASCII("golang") is "golang".
135136
func toASCII(s string) (string, error) {
136-
if ascii(s) {
137+
if ascii.Is(s) {
137138
return s, nil
138139
}
139140
labels := strings.Split(s, ".")
140141
for i, label := range labels {
141-
if !ascii(label) {
142+
if !ascii.Is(label) {
142143
a, err := encode(acePrefix, label)
143144
if err != nil {
144145
return "", err
@@ -148,12 +149,3 @@ func toASCII(s string) (string, error) {
148149
}
149150
return strings.Join(labels, "."), nil
150151
}
151-
152-
func ascii(s string) bool {
153-
for i := 0; i < len(s); i++ {
154-
if s[i] >= utf8.RuneSelf {
155-
return false
156-
}
157-
}
158-
return true
159-
}

src/net/http/header.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package http
77
import (
88
"io"
99
"net/http/httptrace"
10+
"net/http/internal/ascii"
1011
"net/textproto"
1112
"sort"
1213
"strings"
@@ -251,7 +252,7 @@ func hasToken(v, token string) bool {
251252
if endPos := sp + len(token); endPos != len(v) && !isTokenBoundary(v[endPos]) {
252253
continue
253254
}
254-
if strings.EqualFold(v[sp:sp+len(token)], token) {
255+
if ascii.EqualFold(v[sp:sp+len(token)], token) {
255256
return true
256257
}
257258
}

src/net/http/http.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,6 @@ func isNotToken(r rune) bool {
6262
return !httpguts.IsTokenRune(r)
6363
}
6464

65-
func isASCII(s string) bool {
66-
for i := 0; i < len(s); i++ {
67-
if s[i] >= utf8.RuneSelf {
68-
return false
69-
}
70-
}
71-
return true
72-
}
73-
7465
// stringContainsCTLByte reports whether s contains any ASCII control character.
7566
func stringContainsCTLByte(s string) bool {
7667
for i := 0; i < len(s); i++ {

src/net/http/httputil/reverseproxy.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"log"
1414
"net"
1515
"net/http"
16+
"net/http/internal/ascii"
1617
"net/textproto"
1718
"net/url"
1819
"strings"
@@ -242,6 +243,10 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
242243
outreq.Close = false
243244

244245
reqUpType := upgradeType(outreq.Header)
246+
if !ascii.IsPrint(reqUpType) {
247+
p.getErrorHandler()(rw, req, fmt.Errorf("client tried to switch to invalid protocol %q", reqUpType))
248+
return
249+
}
245250
removeConnectionHeaders(outreq.Header)
246251

247252
// Remove hop-by-hop headers to the backend. Especially
@@ -538,13 +543,16 @@ func upgradeType(h http.Header) string {
538543
if !httpguts.HeaderValuesContainsToken(h["Connection"], "Upgrade") {
539544
return ""
540545
}
541-
return strings.ToLower(h.Get("Upgrade"))
546+
return h.Get("Upgrade")
542547
}
543548

544549
func (p *ReverseProxy) handleUpgradeResponse(rw http.ResponseWriter, req *http.Request, res *http.Response) {
545550
reqUpType := upgradeType(req.Header)
546551
resUpType := upgradeType(res.Header)
547-
if reqUpType != resUpType {
552+
if !ascii.IsPrint(resUpType) { // We know reqUpType is ASCII, it's checked by the caller.
553+
p.getErrorHandler()(rw, req, fmt.Errorf("backend tried to switch to invalid protocol %q", resUpType))
554+
}
555+
if !ascii.EqualFold(reqUpType, resUpType) {
548556
p.getErrorHandler()(rw, req, fmt.Errorf("backend tried to switch protocol %q when %q was requested", resUpType, reqUpType))
549557
return
550558
}

src/net/http/httputil/reverseproxy_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"log"
1717
"net/http"
1818
"net/http/httptest"
19+
"net/http/internal/ascii"
1920
"net/url"
2021
"os"
2122
"reflect"
@@ -1183,7 +1184,7 @@ func TestReverseProxyWebSocket(t *testing.T) {
11831184
t.Errorf("Header(XHeader) = %q; want %q", got, want)
11841185
}
11851186

1186-
if upgradeType(res.Header) != "websocket" {
1187+
if !ascii.EqualFold(upgradeType(res.Header), "websocket") {
11871188
t.Fatalf("not websocket upgrade; got %#v", res.Header)
11881189
}
11891190
rwc, ok := res.Body.(io.ReadWriteCloser)
@@ -1300,7 +1301,7 @@ func TestReverseProxyWebSocketCancelation(t *testing.T) {
13001301
t.Errorf("X-Header mismatch\n\tgot: %q\n\twant: %q", g, w)
13011302
}
13021303

1303-
if g, w := upgradeType(res.Header), "websocket"; g != w {
1304+
if g, w := upgradeType(res.Header), "websocket"; !ascii.EqualFold(g, w) {
13041305
t.Fatalf("Upgrade header mismatch\n\tgot: %q\n\twant: %q", g, w)
13051306
}
13061307

src/net/http/internal/ascii/print.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2021 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 ascii
6+
7+
import (
8+
"strings"
9+
"unicode"
10+
)
11+
12+
// EqualFold is strings.EqualFold, ASCII only. It reports whether s and t
13+
// are equal, ASCII-case-insensitively.
14+
func EqualFold(s, t string) bool {
15+
if len(s) != len(t) {
16+
return false
17+
}
18+
for i := 0; i < len(s); i++ {
19+
if lower(s[i]) != lower(t[i]) {
20+
return false
21+
}
22+
}
23+
return true
24+
}
25+
26+
// lower returns the ASCII lowercase version of b.
27+
func lower(b byte) byte {
28+
if 'A' <= b && b <= 'Z' {
29+
return b + ('a' - 'A')
30+
}
31+
return b
32+
}
33+
34+
// IsPrint returns whether s is ASCII and printable according to
35+
// https://tools.ietf.org/html/rfc20#section-4.2.
36+
func IsPrint(s string) bool {
37+
for i := 0; i < len(s); i++ {
38+
if s[i] < ' ' || s[i] > '~' {
39+
return false
40+
}
41+
}
42+
return true
43+
}
44+
45+
// Is returns whether s is ASCII.
46+
func Is(s string) bool {
47+
for i := 0; i < len(s); i++ {
48+
if s[i] > unicode.MaxASCII {
49+
return false
50+
}
51+
}
52+
return true
53+
}
54+
55+
// ToLower returns the lowercase version of s if s is ASCII and printable.
56+
func ToLower(s string) (lower string, ok bool) {
57+
if !IsPrint(s) {
58+
return "", false
59+
}
60+
return strings.ToLower(s), true
61+
}

0 commit comments

Comments
 (0)