Skip to content

Commit 5c622a5

Browse files
committed
cmd/go/internal/modfetch: restrict file names in zip files, avoid case-insensitive collisions
Within the zip file for a given module, disallow names that are invalid on various operating systems (mostly Windows), and disallow having two different paths that are case-fold-equivalent. Disallowing different case-fold-equivalent paths means the zip file content is safe for case-insensitive file systems. There is more we could do to relax the rules later, but I think this should be enough to avoid digging a hole in the early days of modules that's hard to climb out of later. In tests on my repo test corpus, the repos now rejected are: github.com/vjeantet/goldap v0.0.0-20160521203625-ea702ca12a40 "doc/RFC 4511 - LDAP: The Protocol.txt": invalid char ':' github.com/ChimeraCoder/anaconda v0.0.0-20160509014622-91bfbf5de08d "json/statuses/show.json?id=404409873170841600": invalid char '?' github.com/bmatcuk/doublestar "test/a☺b": invalid char '☺' github.com/kubernetes-incubator/service-catalog v0.1.10 "cmd/svcat/testdata/responses/clusterserviceclasses?fieldSelector=spec.externalName=user-provided-service.json": invalid char '?' The : and ? are reserved on Windows, and the : is half-reserved (and quite confusing) on macOS. The ☺ is perhaps an overreach, but I am not convinced that allowing all of category So is safe; certainly Sk is not. Change-Id: I83b6ac47ce6c442f726f1036bccccdb15553c0af Reviewed-on: https://go-review.googlesource.com/124380 Run-TryBot: Russ Cox <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 5760ffc commit 5c622a5

File tree

9 files changed

+287
-110
lines changed

9 files changed

+287
-110
lines changed

src/cmd/go/internal/modfetch/unzip.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"strings"
1717

1818
"cmd/go/internal/modfetch/codehost"
19+
"cmd/go/internal/module"
1920
"cmd/go/internal/str"
2021
)
2122

@@ -49,7 +50,28 @@ func Unzip(dir, zipfile, prefix string, maxSize int64) error {
4950
return fmt.Errorf("unzip %v: %s", zipfile, err)
5051
}
5152

52-
// Check total size.
53+
foldPath := make(map[string]string)
54+
var checkFold func(string) error
55+
checkFold = func(name string) error {
56+
fold := str.ToFold(name)
57+
if foldPath[fold] == name {
58+
return nil
59+
}
60+
dir := path.Dir(name)
61+
if dir != "." {
62+
if err := checkFold(dir); err != nil {
63+
return err
64+
}
65+
}
66+
if foldPath[fold] == "" {
67+
foldPath[fold] = name
68+
return nil
69+
}
70+
other := foldPath[fold]
71+
return fmt.Errorf("unzip %v: case-insensitive file name collision: %q and %q", zipfile, other, name)
72+
}
73+
74+
// Check total size, valid file names.
5375
var size int64
5476
for _, zf := range z.File {
5577
if !str.HasPathPrefix(zf.Name, prefix) {
@@ -58,6 +80,13 @@ func Unzip(dir, zipfile, prefix string, maxSize int64) error {
5880
if zf.Name == prefix || strings.HasSuffix(zf.Name, "/") {
5981
continue
6082
}
83+
name := zf.Name[len(prefix)+1:]
84+
if err := module.CheckFilePath(name); err != nil {
85+
return fmt.Errorf("unzip %v: %v", zipfile, err)
86+
}
87+
if err := checkFold(name); err != nil {
88+
return err
89+
}
6190
if path.Clean(zf.Name) != zf.Name || strings.HasPrefix(zf.Name[len(prefix)+1:], "/") {
6291
return fmt.Errorf("unzip %v: invalid file name %s", zipfile, zf.Name)
6392
}

src/cmd/go/internal/module/module.go

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"fmt"
1919
"sort"
2020
"strings"
21+
"unicode"
2122
"unicode/utf8"
2223

2324
"cmd/go/internal/semver"
@@ -85,24 +86,53 @@ func firstPathOK(r rune) bool {
8586
'a' <= r && r <= 'z'
8687
}
8788

88-
// pathOK reports whether r can appear in a module path.
89-
// Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: + - . / _ and ~.
89+
// pathOK reports whether r can appear in an import path element.
90+
// Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: + - . _ and ~.
9091
// This matches what "go get" has historically recognized in import paths.
9192
// TODO(rsc): We would like to allow Unicode letters, but that requires additional
9293
// care in the safe encoding (see note below).
9394
func pathOK(r rune) bool {
9495
if r < utf8.RuneSelf {
95-
return r == '+' || r == '-' || r == '.' || r == '/' || r == '_' || r == '~' ||
96+
return r == '+' || r == '-' || r == '.' || r == '_' || r == '~' ||
9697
'0' <= r && r <= '9' ||
9798
'A' <= r && r <= 'Z' ||
9899
'a' <= r && r <= 'z'
99100
}
100101
return false
101102
}
102103

104+
// fileNameOK reports whether r can appear in a file name.
105+
// For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters.
106+
// If we expand the set of allowed characters here, we have to
107+
// work harder at detecting potential case-folding and normalization collisions.
108+
// See note about "safe encoding" below.
109+
func fileNameOK(r rune) bool {
110+
if r < utf8.RuneSelf {
111+
// Entire set of ASCII punctuation, from which we remove characters:
112+
// ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
113+
// We disallow some shell special characters: " ' * < > ? ` |
114+
// (Note that some of those are disallowed by the Windows file system as well.)
115+
// We also disallow path separators / : and \ (fileNameOK is only called on path element characters).
116+
// We allow spaces (U+0020) in file names.
117+
const allowed = "!#$%&()+,-.=@[]^_{}~ "
118+
if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
119+
return true
120+
}
121+
for i := 0; i < len(allowed); i++ {
122+
if rune(allowed[i]) == r {
123+
return true
124+
}
125+
}
126+
return false
127+
}
128+
// It may be OK to add more ASCII punctuation here, but only carefully.
129+
// For example Windows disallows < > \, and macOS disallows :, so we must not allow those.
130+
return unicode.IsLetter(r)
131+
}
132+
103133
// CheckPath checks that a module path is valid.
104134
func CheckPath(path string) error {
105-
if err := checkImportPath(path); err != nil {
135+
if err := checkPath(path, false); err != nil {
106136
return fmt.Errorf("malformed module path %q: %v", path, err)
107137
}
108138
i := strings.Index(path, "/")
@@ -131,17 +161,19 @@ func CheckPath(path string) error {
131161

132162
// CheckImportPath checks that an import path is valid.
133163
func CheckImportPath(path string) error {
134-
if err := checkImportPath(path); err != nil {
164+
if err := checkPath(path, false); err != nil {
135165
return fmt.Errorf("malformed import path %q: %v", path, err)
136166
}
137167
return nil
138168
}
139169

140-
// checkImportPath checks that an import path is valid.
170+
// checkPath checks that a general path is valid.
141171
// It returns an error describing why but not mentioning path.
142172
// Because these checks apply to both module paths and import paths,
143173
// the caller is expected to add the "malformed ___ path %q: " prefix.
144-
func checkImportPath(path string) error {
174+
// fileName indicates whether the final element of the path is a file name
175+
// (as opposed to a directory name).
176+
func checkPath(path string, fileName bool) error {
145177
if !utf8.ValidString(path) {
146178
return fmt.Errorf("invalid UTF-8")
147179
}
@@ -159,33 +191,43 @@ func checkImportPath(path string) error {
159191
}
160192
elemStart := 0
161193
for i, r := range path {
162-
if !pathOK(r) {
163-
return fmt.Errorf("invalid char %q", r)
164-
}
165194
if r == '/' {
166-
if err := checkElem(path[elemStart:i]); err != nil {
195+
if err := checkElem(path[elemStart:i], fileName); err != nil {
167196
return err
168197
}
169198
elemStart = i + 1
170199
}
171200
}
172-
if err := checkElem(path[elemStart:]); err != nil {
201+
if err := checkElem(path[elemStart:], fileName); err != nil {
173202
return err
174203
}
175204
return nil
176205
}
177206

178207
// checkElem checks whether an individual path element is valid.
179-
func checkElem(elem string) error {
208+
// fileName indicates whether the element is a file name (not a directory name).
209+
func checkElem(elem string, fileName bool) error {
180210
if elem == "" {
181211
return fmt.Errorf("empty path element")
182212
}
183-
if elem[0] == '.' {
213+
if strings.Count(elem, ".") == len(elem) {
214+
return fmt.Errorf("invalid path element %q", elem)
215+
}
216+
if elem[0] == '.' && !fileName {
184217
return fmt.Errorf("leading dot in path element")
185218
}
186219
if elem[len(elem)-1] == '.' {
187220
return fmt.Errorf("trailing dot in path element")
188221
}
222+
charOK := pathOK
223+
if fileName {
224+
charOK = fileNameOK
225+
}
226+
for _, r := range elem {
227+
if !charOK(r) {
228+
return fmt.Errorf("invalid char %q", r)
229+
}
230+
}
189231

190232
// Windows disallows a bunch of path elements, sadly.
191233
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
@@ -201,6 +243,14 @@ func checkElem(elem string) error {
201243
return nil
202244
}
203245

246+
// CheckFilePath checks whether a slash-separated file path is valid.
247+
func CheckFilePath(path string) error {
248+
if err := checkPath(path, true); err != nil {
249+
return fmt.Errorf("malformed file path %q: %v", path, err)
250+
}
251+
return nil
252+
}
253+
204254
// badWindowsNames are the reserved file path elements on Windows.
205255
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
206256
var badWindowsNames = []string{

0 commit comments

Comments
 (0)