Skip to content

Commit 829c5df

Browse files
committed
net/url, net/http: reject control characters in URLs
This is a more conservative version of the reverted CL 99135 (which was reverted in CL 137716) The net/url part rejects URLs with ASCII CTLs from being parsed and the net/http part rejects writing them if a bogus url.URL is constructed otherwise. Updates #27302 Updates #22907 Change-Id: I09a2212eb74c63db575223277aec363c55421ed8 Reviewed-on: https://go-review.googlesource.com/c/159157 Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Filippo Valsorda <[email protected]>
1 parent 4edea0f commit 829c5df

File tree

6 files changed

+60
-6
lines changed

6 files changed

+60
-6
lines changed

src/net/http/fs_test.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -583,16 +583,23 @@ func TestFileServerZeroByte(t *testing.T) {
583583
ts := httptest.NewServer(FileServer(Dir(".")))
584584
defer ts.Close()
585585

586-
res, err := Get(ts.URL + "/..\x00")
586+
c, err := net.Dial("tcp", ts.Listener.Addr().String())
587587
if err != nil {
588588
t.Fatal(err)
589589
}
590-
b, err := ioutil.ReadAll(res.Body)
590+
defer c.Close()
591+
_, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n")
592+
if err != nil {
593+
t.Fatal(err)
594+
}
595+
var got bytes.Buffer
596+
bufr := bufio.NewReader(io.TeeReader(c, &got))
597+
res, err := ReadResponse(bufr, nil)
591598
if err != nil {
592-
t.Fatal("reading Body:", err)
599+
t.Fatal("ReadResponse: ", err)
593600
}
594601
if res.StatusCode == 200 {
595-
t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
602+
t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes())
596603
}
597604
}
598605

src/net/http/http.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ func isASCII(s string) bool {
5959
return true
6060
}
6161

62+
// isCTL reports whether r is an ASCII control character, including
63+
// the Extended ASCII control characters included in Unicode.
64+
func isCTL(r rune) bool {
65+
return r < ' ' || 0x7f <= r && r <= 0x9f
66+
}
67+
6268
func hexEscapeNonASCII(s string) string {
6369
newLen := 0
6470
for i := 0; i < len(s); i++ {

src/net/http/request.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,12 @@ func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitF
550550
ruri = r.URL.Opaque
551551
}
552552
}
553-
// TODO(bradfitz): escape at least newlines in ruri?
553+
if strings.IndexFunc(ruri, isCTL) != -1 {
554+
return errors.New("net/http: can't write control character in Request.URL")
555+
}
556+
// TODO: validate r.Method too? At least it's less likely to
557+
// come from an attacker (more likely to be a constant in
558+
// code).
554559

555560
// Wrap the writer in a bufio Writer if it's not already buffered.
556561
// Don't always call NewWriter, as that forces a bytes.Buffer

src/net/http/requestwrite_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,17 @@ var reqWriteTests = []reqWriteTest{
576576
"User-Agent: Go-http-client/1.1\r\n" +
577577
"X-Foo: X-Bar\r\n\r\n",
578578
},
579+
580+
25: {
581+
Req: Request{
582+
Method: "GET",
583+
URL: &url.URL{
584+
Host: "www.example.com",
585+
RawQuery: "new\nline", // or any CTL
586+
},
587+
},
588+
WantError: errors.New("net/http: can't write control character in Request.URL"),
589+
},
579590
}
580591

581592
func TestRequestWrite(t *testing.T) {

src/net/url/url.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,10 @@ func parse(rawurl string, viaRequest bool) (*URL, error) {
513513
var rest string
514514
var err error
515515

516+
if strings.IndexFunc(rawurl, isCTL) != -1 {
517+
return nil, errors.New("net/url: invalid control character in URL")
518+
}
519+
516520
if rawurl == "" && viaRequest {
517521
return nil, errors.New("empty url")
518522
}
@@ -1134,3 +1138,9 @@ func validUserinfo(s string) bool {
11341138
}
11351139
return true
11361140
}
1141+
1142+
// isCTL reports whether r is an ASCII control character, including
1143+
// the Extended ASCII control characters included in Unicode.
1144+
func isCTL(r rune) bool {
1145+
return r < ' ' || 0x7f <= r && r <= 0x9f
1146+
}

src/net/url/url_test.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1738,12 +1738,27 @@ func TestNilUser(t *testing.T) {
17381738
}
17391739

17401740
func TestInvalidUserPassword(t *testing.T) {
1741-
_, err := Parse("http://us\ner:pass\nword@foo.com/")
1741+
_, err := Parse("http://user^:passwo^rd@foo.com/")
17421742
if got, wantsub := fmt.Sprint(err), "net/url: invalid userinfo"; !strings.Contains(got, wantsub) {
17431743
t.Errorf("error = %q; want substring %q", got, wantsub)
17441744
}
17451745
}
17461746

1747+
func TestRejectControlCharacters(t *testing.T) {
1748+
tests := []string{
1749+
"http://foo.com/?foo\nbar",
1750+
"http\r://foo.com/",
1751+
"http://foo\x7f.com/",
1752+
}
1753+
for _, s := range tests {
1754+
_, err := Parse(s)
1755+
const wantSub = "net/url: invalid control character in URL"
1756+
if got := fmt.Sprint(err); !strings.Contains(got, wantSub) {
1757+
t.Errorf("Parse(%q) error = %q; want substring %q", s, got, wantSub)
1758+
}
1759+
}
1760+
}
1761+
17471762
var escapeBenchmarks = []struct {
17481763
unescaped string
17491764
query string

0 commit comments

Comments
 (0)