Skip to content

Commit ce7ea0a

Browse files
samthanawallagopherbot
authored andcommitted
cmd/go: refine GOAUTH user parsing to be more strict
This CL enhances the parsing of GOAUTH user based authentication for improved security. Updates: #26232 Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-windows-amd64-longtest Change-Id: Ica57952924020b7bd2670610af8de8ce52dbe92f Reviewed-on: https://go-review.googlesource.com/c/go/+/644995 Auto-Submit: Sam Thanawalla <[email protected]> Reviewed-by: Damien Neil <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent e81f715 commit ce7ea0a

File tree

5 files changed

+266
-60
lines changed

5 files changed

+266
-60
lines changed

Diff for: src/cmd/go/alldocs.go

+1-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: src/cmd/go/internal/auth/httputils.go

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2019 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+
// Code copied from x/net/http/httpguts/httplex.go
6+
package auth
7+
8+
var isTokenTable = [256]bool{
9+
'!': true,
10+
'#': true,
11+
'$': true,
12+
'%': true,
13+
'&': true,
14+
'\'': true,
15+
'*': true,
16+
'+': true,
17+
'-': true,
18+
'.': true,
19+
'0': true,
20+
'1': true,
21+
'2': true,
22+
'3': true,
23+
'4': true,
24+
'5': true,
25+
'6': true,
26+
'7': true,
27+
'8': true,
28+
'9': true,
29+
'A': true,
30+
'B': true,
31+
'C': true,
32+
'D': true,
33+
'E': true,
34+
'F': true,
35+
'G': true,
36+
'H': true,
37+
'I': true,
38+
'J': true,
39+
'K': true,
40+
'L': true,
41+
'M': true,
42+
'N': true,
43+
'O': true,
44+
'P': true,
45+
'Q': true,
46+
'R': true,
47+
'S': true,
48+
'T': true,
49+
'U': true,
50+
'W': true,
51+
'V': true,
52+
'X': true,
53+
'Y': true,
54+
'Z': true,
55+
'^': true,
56+
'_': true,
57+
'`': true,
58+
'a': true,
59+
'b': true,
60+
'c': true,
61+
'd': true,
62+
'e': true,
63+
'f': true,
64+
'g': true,
65+
'h': true,
66+
'i': true,
67+
'j': true,
68+
'k': true,
69+
'l': true,
70+
'm': true,
71+
'n': true,
72+
'o': true,
73+
'p': true,
74+
'q': true,
75+
'r': true,
76+
's': true,
77+
't': true,
78+
'u': true,
79+
'v': true,
80+
'w': true,
81+
'x': true,
82+
'y': true,
83+
'z': true,
84+
'|': true,
85+
'~': true,
86+
}
87+
88+
// isLWS reports whether b is linear white space, according
89+
// to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
90+
//
91+
// LWS = [CRLF] 1*( SP | HT )
92+
func isLWS(b byte) bool { return b == ' ' || b == '\t' }
93+
94+
// isCTL reports whether b is a control byte, according
95+
// to http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2
96+
//
97+
// CTL = <any US-ASCII control character
98+
// (octets 0 - 31) and DEL (127)>
99+
func isCTL(b byte) bool {
100+
const del = 0x7f // a CTL
101+
return b < ' ' || b == del
102+
}
103+
104+
// validHeaderFieldName reports whether v is a valid HTTP/1.x header name.
105+
// HTTP/2 imposes the additional restriction that uppercase ASCII
106+
// letters are not allowed.
107+
//
108+
// RFC 7230 says:
109+
//
110+
// header-field = field-name ":" OWS field-value OWS
111+
// field-name = token
112+
// token = 1*tchar
113+
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
114+
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
115+
func validHeaderFieldName(v string) bool {
116+
if len(v) == 0 {
117+
return false
118+
}
119+
for i := 0; i < len(v); i++ {
120+
if !isTokenTable[v[i]] {
121+
return false
122+
}
123+
}
124+
return true
125+
}
126+
127+
// validHeaderFieldValue reports whether v is a valid "field-value" according to
128+
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 :
129+
//
130+
// message-header = field-name ":" [ field-value ]
131+
// field-value = *( field-content | LWS )
132+
// field-content = <the OCTETs making up the field-value
133+
// and consisting of either *TEXT or combinations
134+
// of token, separators, and quoted-string>
135+
//
136+
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2.2 :
137+
//
138+
// TEXT = <any OCTET except CTLs,
139+
// but including LWS>
140+
// LWS = [CRLF] 1*( SP | HT )
141+
// CTL = <any US-ASCII control character
142+
// (octets 0 - 31) and DEL (127)>
143+
//
144+
// RFC 7230 says:
145+
//
146+
// field-value = *( field-content / obs-fold )
147+
// obj-fold = N/A to http2, and deprecated
148+
// field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
149+
// field-vchar = VCHAR / obs-text
150+
// obs-text = %x80-FF
151+
// VCHAR = "any visible [USASCII] character"
152+
//
153+
// http2 further says: "Similarly, HTTP/2 allows header field values
154+
// that are not valid. While most of the values that can be encoded
155+
// will not alter header field parsing, carriage return (CR, ASCII
156+
// 0xd), line feed (LF, ASCII 0xa), and the zero character (NUL, ASCII
157+
// 0x0) might be exploited by an attacker if they are translated
158+
// verbatim. Any request or response that contains a character not
159+
// permitted in a header field value MUST be treated as malformed
160+
// (Section 8.1.2.6). Valid characters are defined by the
161+
// field-content ABNF rule in Section 3.2 of [RFC7230]."
162+
//
163+
// This function does not (yet?) properly handle the rejection of
164+
// strings that begin or end with SP or HTAB.
165+
func validHeaderFieldValue(v string) bool {
166+
for i := 0; i < len(v); i++ {
167+
b := v[i]
168+
if isCTL(b) && !isLWS(b) {
169+
return false
170+
}
171+
}
172+
return true
173+
}

Diff for: src/cmd/go/internal/auth/userauth.go

+41-50
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@
66
package auth
77

88
import (
9-
"bufio"
10-
"bytes"
119
"cmd/internal/quoted"
1210
"fmt"
13-
"io"
1411
"maps"
1512
"net/http"
16-
"net/textproto"
13+
"net/url"
1714
"os/exec"
1815
"strings"
1916
)
@@ -42,7 +39,7 @@ func runAuthCommand(command string, url string, res *http.Response) (map[string]
4239
if err != nil {
4340
return nil, fmt.Errorf("could not run command %s: %v\n%s", command, err, cmd.Stderr)
4441
}
45-
credentials, err := parseUserAuth(bytes.NewReader(out))
42+
credentials, err := parseUserAuth(string(out))
4643
if err != nil {
4744
return nil, fmt.Errorf("cannot parse output of GOAUTH command %s: %v", command, err)
4845
}
@@ -54,53 +51,47 @@ func runAuthCommand(command string, url string, res *http.Response) (map[string]
5451
// or an error if the data does not follow the expected format.
5552
// Returns an nil error and an empty map if the data is empty.
5653
// See the expected format in 'go help goauth'.
57-
func parseUserAuth(data io.Reader) (map[string]http.Header, error) {
54+
func parseUserAuth(data string) (map[string]http.Header, error) {
5855
credentials := make(map[string]http.Header)
59-
reader := textproto.NewReader(bufio.NewReader(data))
60-
for {
61-
// Return the processed credentials if the reader is at EOF.
62-
if _, err := reader.R.Peek(1); err == io.EOF {
63-
return credentials, nil
56+
for data != "" {
57+
var line string
58+
var ok bool
59+
var urls []string
60+
// Parse URLS first.
61+
for {
62+
line, data, ok = strings.Cut(data, "\n")
63+
if !ok {
64+
return nil, fmt.Errorf("invalid format: missing empty line after URLs")
65+
}
66+
if line == "" {
67+
break
68+
}
69+
u, err := url.ParseRequestURI(line)
70+
if err != nil {
71+
return nil, fmt.Errorf("could not parse URL %s: %v", line, err)
72+
}
73+
urls = append(urls, u.String())
6474
}
65-
urls, err := readURLs(reader)
66-
if err != nil {
67-
return nil, err
68-
}
69-
if len(urls) == 0 {
70-
return nil, fmt.Errorf("invalid format: expected url prefix")
71-
}
72-
mimeHeader, err := reader.ReadMIMEHeader()
73-
if err != nil {
74-
return nil, err
75-
}
76-
header := http.Header(mimeHeader)
77-
// Process the block (urls and headers).
78-
credentialMap := mapHeadersToPrefixes(urls, header)
79-
maps.Copy(credentials, credentialMap)
80-
}
81-
}
82-
83-
// readURLs reads URL prefixes from the given reader until an empty line
84-
// is encountered or an error occurs. It returns the list of URLs or an error
85-
// if the format is invalid.
86-
func readURLs(reader *textproto.Reader) (urls []string, err error) {
87-
for {
88-
line, err := reader.ReadLine()
89-
if err != nil {
90-
return nil, err
91-
}
92-
trimmedLine := strings.TrimSpace(line)
93-
if trimmedLine != line {
94-
return nil, fmt.Errorf("invalid format: leading or trailing white space")
95-
}
96-
if strings.HasPrefix(line, "https://") {
97-
urls = append(urls, line)
98-
} else if line == "" {
99-
return urls, nil
100-
} else {
101-
return nil, fmt.Errorf("invalid format: expected url prefix or empty line")
75+
// Parse Headers second.
76+
header := make(http.Header)
77+
for {
78+
line, data, ok = strings.Cut(data, "\n")
79+
if !ok {
80+
return nil, fmt.Errorf("invalid format: missing empty line after headers")
81+
}
82+
if line == "" {
83+
break
84+
}
85+
name, value, ok := strings.Cut(line, ": ")
86+
value = strings.TrimSpace(value)
87+
if !ok || !validHeaderFieldName(name) || !validHeaderFieldValue(value) {
88+
return nil, fmt.Errorf("invalid format: invalid header line")
89+
}
90+
header.Add(name, value)
10291
}
92+
maps.Copy(credentials, mapHeadersToPrefixes(urls, header))
10393
}
94+
return credentials, nil
10495
}
10596

10697
// mapHeadersToPrefixes returns a mapping of prefix → http.Header without
@@ -127,8 +118,8 @@ func buildCommand(command string) (*exec.Cmd, error) {
127118
func writeResponseToStdin(cmd *exec.Cmd, res *http.Response) error {
128119
var output strings.Builder
129120
output.WriteString(res.Proto + " " + res.Status + "\n")
130-
if err := res.Header.Write(&output); err != nil {
131-
return err
121+
for k, v := range res.Header {
122+
output.WriteString(k + ": " + strings.Join(v, ", ") + "\n")
132123
}
133124
output.WriteString("\n")
134125
cmd.Stdin = strings.NewReader(output.String())

0 commit comments

Comments
 (0)