Commit 703166ea authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/url: validate ports in URLs and bytes after IPv6 literals

Fixes #11208

Change-Id: I35cc94129577b2a977fd35aafb0a5fb02c534a7c
Reviewed-on: https://go-review.googlesource.com/11414Reviewed-by: 's avatarDmitry Vyukov <dvyukov@google.com>
parent 222b23aa
...@@ -362,3 +362,11 @@ func TestReadRequest(t *testing.T) { ...@@ -362,3 +362,11 @@ func TestReadRequest(t *testing.T) {
} }
} }
} }
func TestReadRequest_BadConnectHost(t *testing.T) {
data := []byte("CONNECT []%20%48%54%54%50%2f%31%2e%31%0a%4d%79%48%65%61%64%65%72%3a%20%31%32%33%0a%0a HTTP/1.0\n\n")
r, err := ReadRequest(bufio.NewReader(bytes.NewReader(data)))
if err == nil {
t.Fatal("Got unexpected request = %#v", r)
}
}
...@@ -9,6 +9,7 @@ package url ...@@ -9,6 +9,7 @@ package url
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
...@@ -142,7 +143,7 @@ func unescape(s string, mode encoding) (string, error) { ...@@ -142,7 +143,7 @@ func unescape(s string, mode encoding) (string, error) {
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
s = s[i:] s = s[i:]
if len(s) > 3 { if len(s) > 3 {
s = s[0:3] s = s[:3]
} }
return "", EscapeError(s) return "", EscapeError(s)
} }
...@@ -328,7 +329,7 @@ func getscheme(rawurl string) (scheme, path string, err error) { ...@@ -328,7 +329,7 @@ func getscheme(rawurl string) (scheme, path string, err error) {
if i == 0 { if i == 0 {
return "", "", errors.New("missing protocol scheme") return "", "", errors.New("missing protocol scheme")
} }
return rawurl[0:i], rawurl[i+1:], nil return rawurl[:i], rawurl[i+1:], nil
default: default:
// we have encountered an invalid character, // we have encountered an invalid character,
// so there is no valid scheme // so there is no valid scheme
...@@ -347,9 +348,9 @@ func split(s string, c string, cutc bool) (string, string) { ...@@ -347,9 +348,9 @@ func split(s string, c string, cutc bool) (string, string) {
return s, "" return s, ""
} }
if cutc { if cutc {
return s[0:i], s[i+len(c):] return s[:i], s[i+len(c):]
} }
return s[0:i], s[i:] return s[:i], s[i:]
} }
// Parse parses rawurl into a URL structure. // Parse parses rawurl into a URL structure.
...@@ -467,9 +468,11 @@ func parseAuthority(authority string) (user *Userinfo, host string, err error) { ...@@ -467,9 +468,11 @@ func parseAuthority(authority string) (user *Userinfo, host string, err error) {
return user, host, nil return user, host, nil
} }
// parseHost parses host as an authority without user information. // parseHost parses host as an authority without user
// information. That is, as host[:port].
func parseHost(host string) (string, error) { func parseHost(host string) (string, error) {
litOrName := host litOrName := host
var colonPort string // ":80" or ""
if strings.HasPrefix(host, "[") { if strings.HasPrefix(host, "[") {
// Parse an IP-Literal in RFC 3986 and RFC 6874. // Parse an IP-Literal in RFC 3986 and RFC 6874.
// E.g., "[fe80::1], "[fe80::1%25en0]" // E.g., "[fe80::1], "[fe80::1%25en0]"
...@@ -477,18 +480,23 @@ func parseHost(host string) (string, error) { ...@@ -477,18 +480,23 @@ func parseHost(host string) (string, error) {
// RFC 4007 defines "%" as a delimiter character in // RFC 4007 defines "%" as a delimiter character in
// the textual representation of IPv6 addresses. // the textual representation of IPv6 addresses.
// Per RFC 6874, in URIs that "%" is encoded as "%25". // Per RFC 6874, in URIs that "%" is encoded as "%25".
i := strings.LastIndex(host[1:], "]") i := strings.LastIndex(host, "]")
if i < 0 { if i < 0 {
return "", errors.New("missing ']' in host") return "", errors.New("missing ']' in host")
} }
colonPort = host[i+1:]
// Parse a host subcomponent without a ZoneID in RFC // Parse a host subcomponent without a ZoneID in RFC
// 6874 because the ZoneID is allowed to use the // 6874 because the ZoneID is allowed to use the
// percent encoded form. // percent encoded form.
j := strings.Index(host[1:1+i], "%25") j := strings.Index(host[:i], "%25")
if j < 0 { if j < 0 {
litOrName = host[1 : 1+i] litOrName = host[1:i]
} else { } else {
litOrName = host[1 : 1+j] litOrName = host[1:j]
}
} else {
if i := strings.Index(host, ":"); i != -1 {
colonPort = host[i:]
} }
} }
// A URI containing an IP-Literal without a ZoneID or // A URI containing an IP-Literal without a ZoneID or
...@@ -503,6 +511,9 @@ func parseHost(host string) (string, error) { ...@@ -503,6 +511,9 @@ func parseHost(host string) (string, error) {
if strings.Contains(litOrName, "%") { if strings.Contains(litOrName, "%") {
return "", errors.New("percent-encoded characters in host") return "", errors.New("percent-encoded characters in host")
} }
if !validOptionalPort(colonPort) {
return "", fmt.Errorf("invalid port %q after host", colonPort)
}
var err error var err error
if host, err = unescape(host, encodeHost); err != nil { if host, err = unescape(host, encodeHost); err != nil {
return "", err return "", err
...@@ -540,6 +551,23 @@ func validEncodedPath(s string) bool { ...@@ -540,6 +551,23 @@ func validEncodedPath(s string) bool {
return true return true
} }
// validOptionalPort reports whether port is either an empty string
// or matches /^:\d+$/
func validOptionalPort(port string) bool {
if port == "" {
return true
}
if port[0] != ':' || len(port) == 1 {
return false
}
for _, b := range port[1:] {
if b < '0' || b > '9' {
return false
}
}
return true
}
// String reassembles the URL into a valid URL string. // String reassembles the URL into a valid URL string.
// The general form of the result is one of: // The general form of the result is one of:
// //
......
...@@ -1074,6 +1074,39 @@ func TestParseFailure(t *testing.T) { ...@@ -1074,6 +1074,39 @@ func TestParseFailure(t *testing.T) {
} }
} }
func TestParseAuthority(t *testing.T) {
tests := []struct {
in string
wantErr bool
}{
{"http://[::1]", false},
{"http://[::1]:80", false},
{"http://[::1]:namedport", true}, // rfc3986 3.2.3
{"http://[::1]/", false},
{"http://[::1]a", true},
{"http://[::1]%23", true},
{"http://[::1%25en0]", false}, // valid zone id
{"http://[::1]:", true}, // colon, but no port
{"http://[::1]:%38%30", true}, // no hex in port
{"http://[::1%25%10]", false}, // TODO: reject the %10 after the valid zone %25 separator?
{"http://[%10::1]", true}, // no %xx escapes in IP address
{"http://[::1]/%48", false}, // %xx in path is fine
{"http://%41:8080/", true}, // TODO: arguably we should accept reg-name with %xx
}
for _, tt := range tests {
u, err := Parse(tt.in)
if tt.wantErr {
if err == nil {
t.Errorf("Parse(%q) = %#v; want an error", tt.in, u)
}
continue
}
if err != nil {
t.Logf("Parse(%q) = %v; want no error", tt.in, err)
}
}
}
type shouldEscapeTest struct { type shouldEscapeTest struct {
in byte in byte
mode encoding mode encoding
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment