diff options
| -rw-r--r-- | meta/recipes-devtools/go/go-1.17.13.inc | 2 | ||||
| -rw-r--r-- | meta/recipes-devtools/go/go-1.18/CVE-2022-2879.patch | 177 | ||||
| -rw-r--r-- | meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch | 514 |
3 files changed, 693 insertions, 0 deletions
diff --git a/meta/recipes-devtools/go/go-1.17.13.inc b/meta/recipes-devtools/go/go-1.17.13.inc index 99662bd298..856c14de40 100644 --- a/meta/recipes-devtools/go/go-1.17.13.inc +++ b/meta/recipes-devtools/go/go-1.17.13.inc | |||
| @@ -20,6 +20,8 @@ SRC_URI += "\ | |||
| 20 | file://0001-net-http-httputil-avoid-query-parameter-smuggling.patch \ | 20 | file://0001-net-http-httputil-avoid-query-parameter-smuggling.patch \ |
| 21 | file://CVE-2022-41715.patch \ | 21 | file://CVE-2022-41715.patch \ |
| 22 | file://CVE-2022-41717.patch \ | 22 | file://CVE-2022-41717.patch \ |
| 23 | file://CVE-2022-2879.patch \ | ||
| 24 | file://CVE-2022-41720.patch \ | ||
| 23 | " | 25 | " |
| 24 | SRC_URI[main.sha256sum] = "a1a48b23afb206f95e7bbaa9b898d965f90826f6f1d1fc0c1d784ada0cd300fd" | 26 | SRC_URI[main.sha256sum] = "a1a48b23afb206f95e7bbaa9b898d965f90826f6f1d1fc0c1d784ada0cd300fd" |
| 25 | 27 | ||
diff --git a/meta/recipes-devtools/go/go-1.18/CVE-2022-2879.patch b/meta/recipes-devtools/go/go-1.18/CVE-2022-2879.patch new file mode 100644 index 0000000000..0315e1a3ee --- /dev/null +++ b/meta/recipes-devtools/go/go-1.18/CVE-2022-2879.patch | |||
| @@ -0,0 +1,177 @@ | |||
| 1 | From d064ed520a7cc6b480f9565e30751e695d394f4e Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Damien Neil <dneil@google.com> | ||
| 3 | Date: Fri, 2 Sep 2022 20:45:18 -0700 | ||
| 4 | Subject: [PATCH] archive/tar: limit size of headers | ||
| 5 | |||
| 6 | Set a 1MiB limit on special file blocks (PAX headers, GNU long names, | ||
| 7 | GNU link names), to avoid reading arbitrarily large amounts of data | ||
| 8 | into memory. | ||
| 9 | |||
| 10 | Thanks to Adam Korczynski (ADA Logics) and OSS-Fuzz for reporting | ||
| 11 | this issue. | ||
| 12 | |||
| 13 | Fixes CVE-2022-2879 | ||
| 14 | Updates #54853 | ||
| 15 | Fixes #55925 | ||
| 16 | |||
| 17 | Change-Id: I85136d6ff1e0af101a112190e027987ab4335680 | ||
| 18 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1565555 | ||
| 19 | Reviewed-by: Tatiana Bradley <tatianabradley@google.com> | ||
| 20 | Run-TryBot: Roland Shoemaker <bracewell@google.com> | ||
| 21 | Reviewed-by: Roland Shoemaker <bracewell@google.com> | ||
| 22 | (cherry picked from commit 6ee768cef6b82adf7a90dcf367a1699ef694f3b2) | ||
| 23 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1590622 | ||
| 24 | Reviewed-by: Damien Neil <dneil@google.com> | ||
| 25 | Reviewed-by: Julie Qiu <julieqiu@google.com> | ||
| 26 | Reviewed-on: https://go-review.googlesource.com/c/go/+/438500 | ||
| 27 | Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org> | ||
| 28 | Reviewed-by: Carlos Amedee <carlos@golang.org> | ||
| 29 | Reviewed-by: Dmitri Shuralyov <dmitshur@google.com> | ||
| 30 | Run-TryBot: Carlos Amedee <carlos@golang.org> | ||
| 31 | TryBot-Result: Gopher Robot <gobot@golang.org> | ||
| 32 | |||
| 33 | CVE: CVE-2022-2879 | ||
| 34 | Upstream-Status: Backport [0a723816cd205576945fa57fbdde7e6532d59d08] | ||
| 35 | Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> | ||
| 36 | --- | ||
| 37 | src/archive/tar/format.go | 4 ++++ | ||
| 38 | src/archive/tar/reader.go | 14 ++++++++++++-- | ||
| 39 | src/archive/tar/reader_test.go | 8 +++++++- | ||
| 40 | src/archive/tar/writer.go | 3 +++ | ||
| 41 | src/archive/tar/writer_test.go | 27 +++++++++++++++++++++++++++ | ||
| 42 | 5 files changed, 53 insertions(+), 3 deletions(-) | ||
| 43 | |||
| 44 | diff --git a/src/archive/tar/format.go b/src/archive/tar/format.go | ||
| 45 | index cfe24a5..6642364 100644 | ||
| 46 | --- a/src/archive/tar/format.go | ||
| 47 | +++ b/src/archive/tar/format.go | ||
| 48 | @@ -143,6 +143,10 @@ const ( | ||
| 49 | blockSize = 512 // Size of each block in a tar stream | ||
| 50 | nameSize = 100 // Max length of the name field in USTAR format | ||
| 51 | prefixSize = 155 // Max length of the prefix field in USTAR format | ||
| 52 | + | ||
| 53 | + // Max length of a special file (PAX header, GNU long name or link). | ||
| 54 | + // This matches the limit used by libarchive. | ||
| 55 | + maxSpecialFileSize = 1 << 20 | ||
| 56 | ) | ||
| 57 | |||
| 58 | // blockPadding computes the number of bytes needed to pad offset up to the | ||
| 59 | diff --git a/src/archive/tar/reader.go b/src/archive/tar/reader.go | ||
| 60 | index 1b1d5b4..f645af8 100644 | ||
| 61 | --- a/src/archive/tar/reader.go | ||
| 62 | +++ b/src/archive/tar/reader.go | ||
| 63 | @@ -103,7 +103,7 @@ func (tr *Reader) next() (*Header, error) { | ||
| 64 | continue // This is a meta header affecting the next header | ||
| 65 | case TypeGNULongName, TypeGNULongLink: | ||
| 66 | format.mayOnlyBe(FormatGNU) | ||
| 67 | - realname, err := io.ReadAll(tr) | ||
| 68 | + realname, err := readSpecialFile(tr) | ||
| 69 | if err != nil { | ||
| 70 | return nil, err | ||
| 71 | } | ||
| 72 | @@ -293,7 +293,7 @@ func mergePAX(hdr *Header, paxHdrs map[string]string) (err error) { | ||
| 73 | // parsePAX parses PAX headers. | ||
| 74 | // If an extended header (type 'x') is invalid, ErrHeader is returned | ||
| 75 | func parsePAX(r io.Reader) (map[string]string, error) { | ||
| 76 | - buf, err := io.ReadAll(r) | ||
| 77 | + buf, err := readSpecialFile(r) | ||
| 78 | if err != nil { | ||
| 79 | return nil, err | ||
| 80 | } | ||
| 81 | @@ -826,6 +826,16 @@ func tryReadFull(r io.Reader, b []byte) (n int, err error) { | ||
| 82 | return n, err | ||
| 83 | } | ||
| 84 | |||
| 85 | +// readSpecialFile is like io.ReadAll except it returns | ||
| 86 | +// ErrFieldTooLong if more than maxSpecialFileSize is read. | ||
| 87 | +func readSpecialFile(r io.Reader) ([]byte, error) { | ||
| 88 | + buf, err := io.ReadAll(io.LimitReader(r, maxSpecialFileSize+1)) | ||
| 89 | + if len(buf) > maxSpecialFileSize { | ||
| 90 | + return nil, ErrFieldTooLong | ||
| 91 | + } | ||
| 92 | + return buf, err | ||
| 93 | +} | ||
| 94 | + | ||
| 95 | // discard skips n bytes in r, reporting an error if unable to do so. | ||
| 96 | func discard(r io.Reader, n int64) error { | ||
| 97 | // If possible, Seek to the last byte before the end of the data section. | ||
| 98 | diff --git a/src/archive/tar/reader_test.go b/src/archive/tar/reader_test.go | ||
| 99 | index 789ddc1..926dc3d 100644 | ||
| 100 | --- a/src/archive/tar/reader_test.go | ||
| 101 | +++ b/src/archive/tar/reader_test.go | ||
| 102 | @@ -6,6 +6,7 @@ package tar | ||
| 103 | |||
| 104 | import ( | ||
| 105 | "bytes" | ||
| 106 | + "compress/bzip2" | ||
| 107 | "crypto/md5" | ||
| 108 | "errors" | ||
| 109 | "fmt" | ||
| 110 | @@ -625,9 +626,14 @@ func TestReader(t *testing.T) { | ||
| 111 | } | ||
| 112 | defer f.Close() | ||
| 113 | |||
| 114 | + var fr io.Reader = f | ||
| 115 | + if strings.HasSuffix(v.file, ".bz2") { | ||
| 116 | + fr = bzip2.NewReader(fr) | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | // Capture all headers and checksums. | ||
| 120 | var ( | ||
| 121 | - tr = NewReader(f) | ||
| 122 | + tr = NewReader(fr) | ||
| 123 | hdrs []*Header | ||
| 124 | chksums []string | ||
| 125 | rdbuf = make([]byte, 8) | ||
| 126 | diff --git a/src/archive/tar/writer.go b/src/archive/tar/writer.go | ||
| 127 | index e80498d..893eac0 100644 | ||
| 128 | --- a/src/archive/tar/writer.go | ||
| 129 | +++ b/src/archive/tar/writer.go | ||
| 130 | @@ -199,6 +199,9 @@ func (tw *Writer) writePAXHeader(hdr *Header, paxHdrs map[string]string) error { | ||
| 131 | flag = TypeXHeader | ||
| 132 | } | ||
| 133 | data := buf.String() | ||
| 134 | + if len(data) > maxSpecialFileSize { | ||
| 135 | + return ErrFieldTooLong | ||
| 136 | + } | ||
| 137 | if err := tw.writeRawFile(name, data, flag, FormatPAX); err != nil || isGlobal { | ||
| 138 | return err // Global headers return here | ||
| 139 | } | ||
| 140 | diff --git a/src/archive/tar/writer_test.go b/src/archive/tar/writer_test.go | ||
| 141 | index a00f02d..4e709e5 100644 | ||
| 142 | --- a/src/archive/tar/writer_test.go | ||
| 143 | +++ b/src/archive/tar/writer_test.go | ||
| 144 | @@ -1006,6 +1006,33 @@ func TestIssue12594(t *testing.T) { | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | +func TestWriteLongHeader(t *testing.T) { | ||
| 149 | + for _, test := range []struct { | ||
| 150 | + name string | ||
| 151 | + h *Header | ||
| 152 | + }{{ | ||
| 153 | + name: "name too long", | ||
| 154 | + h: &Header{Name: strings.Repeat("a", maxSpecialFileSize)}, | ||
| 155 | + }, { | ||
| 156 | + name: "linkname too long", | ||
| 157 | + h: &Header{Linkname: strings.Repeat("a", maxSpecialFileSize)}, | ||
| 158 | + }, { | ||
| 159 | + name: "uname too long", | ||
| 160 | + h: &Header{Uname: strings.Repeat("a", maxSpecialFileSize)}, | ||
| 161 | + }, { | ||
| 162 | + name: "gname too long", | ||
| 163 | + h: &Header{Gname: strings.Repeat("a", maxSpecialFileSize)}, | ||
| 164 | + }, { | ||
| 165 | + name: "PAX header too long", | ||
| 166 | + h: &Header{PAXRecords: map[string]string{"GOLANG.x": strings.Repeat("a", maxSpecialFileSize)}}, | ||
| 167 | + }} { | ||
| 168 | + w := NewWriter(io.Discard) | ||
| 169 | + if err := w.WriteHeader(test.h); err != ErrFieldTooLong { | ||
| 170 | + t.Errorf("%v: w.WriteHeader() = %v, want ErrFieldTooLong", test.name, err) | ||
| 171 | + } | ||
| 172 | + } | ||
| 173 | +} | ||
| 174 | + | ||
| 175 | // testNonEmptyWriter wraps an io.Writer and ensures that | ||
| 176 | // Write is never called with an empty buffer. | ||
| 177 | type testNonEmptyWriter struct{ io.Writer } | ||
diff --git a/meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch b/meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch new file mode 100644 index 0000000000..6c2e8804b3 --- /dev/null +++ b/meta/recipes-devtools/go/go-1.18/CVE-2022-41720.patch | |||
| @@ -0,0 +1,514 @@ | |||
| 1 | From f8896a97a0630b0f2f8c488310147f7f20b3ec7d Mon Sep 17 00:00:00 2001 | ||
| 2 | From: Damien Neil <dneil@google.com> | ||
| 3 | Date: Thu, 10 Nov 2022 12:16:27 -0800 | ||
| 4 | Subject: [PATCH] os, net/http: avoid escapes from os.DirFS and http.Dir on | ||
| 5 | Windows | ||
| 6 | |||
| 7 | Do not permit access to Windows reserved device names (NUL, COM1, etc.) | ||
| 8 | via os.DirFS and http.Dir filesystems. | ||
| 9 | |||
| 10 | Avoid escapes from os.DirFS(`\`) on Windows. DirFS would join the | ||
| 11 | the root to the relative path with a path separator, making | ||
| 12 | os.DirFS(`\`).Open(`/foo/bar`) open the path `\\foo\bar`, which is | ||
| 13 | a UNC name. Not only does this not open the intended file, but permits | ||
| 14 | reference to any file on the system rather than only files on the | ||
| 15 | current drive. | ||
| 16 | |||
| 17 | Make os.DirFS("") invalid, with all file access failing. Previously, | ||
| 18 | a root of "" was interpreted as "/", which is surprising and probably | ||
| 19 | unintentional. | ||
| 20 | |||
| 21 | Fixes CVE-2022-41720. | ||
| 22 | Fixes #56694. | ||
| 23 | |||
| 24 | Change-Id: I275b5fa391e6ad7404309ea98ccc97405942e0f0 | ||
| 25 | Reviewed-on: https://team-review.git.corp.google.com/c/golang/go-private/+/1663832 | ||
| 26 | Reviewed-by: Julie Qiu <julieqiu@google.com> | ||
| 27 | Reviewed-by: Tatiana Bradley <tatianabradley@google.com> | ||
| 28 | Reviewed-on: https://go-review.googlesource.com/c/go/+/455360 | ||
| 29 | Reviewed-by: Michael Pratt <mpratt@google.com> | ||
| 30 | TryBot-Result: Gopher Robot <gobot@golang.org> | ||
| 31 | Run-TryBot: Jenny Rakoczy <jenny@golang.org> | ||
| 32 | |||
| 33 | CVE: CVE-2022-41720 | ||
| 34 | Upstream-Status: Backport [7013a4f5f816af62033ad63dd06b77c30d7a62a7] | ||
| 35 | Signed-off-by: Sakib Sajal <sakib.sajal@windriver.com> | ||
| 36 | --- | ||
| 37 | src/go/build/deps_test.go | 1 + | ||
| 38 | src/internal/safefilepath/path.go | 21 +++++ | ||
| 39 | src/internal/safefilepath/path_other.go | 23 ++++++ | ||
| 40 | src/internal/safefilepath/path_test.go | 88 +++++++++++++++++++++ | ||
| 41 | src/internal/safefilepath/path_windows.go | 95 +++++++++++++++++++++++ | ||
| 42 | src/net/http/fs.go | 8 +- | ||
| 43 | src/net/http/fs_test.go | 28 +++++++ | ||
| 44 | src/os/file.go | 36 +++++++-- | ||
| 45 | src/os/os_test.go | 38 +++++++++ | ||
| 46 | 9 files changed, 328 insertions(+), 10 deletions(-) | ||
| 47 | create mode 100644 src/internal/safefilepath/path.go | ||
| 48 | create mode 100644 src/internal/safefilepath/path_other.go | ||
| 49 | create mode 100644 src/internal/safefilepath/path_test.go | ||
| 50 | create mode 100644 src/internal/safefilepath/path_windows.go | ||
| 51 | |||
| 52 | diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go | ||
| 53 | index 45e2f25..dc3bb8c 100644 | ||
| 54 | --- a/src/go/build/deps_test.go | ||
| 55 | +++ b/src/go/build/deps_test.go | ||
| 56 | @@ -165,6 +165,7 @@ var depsRules = ` | ||
| 57 | io/fs | ||
| 58 | < internal/testlog | ||
| 59 | < internal/poll | ||
| 60 | + < internal/safefilepath | ||
| 61 | < os | ||
| 62 | < os/signal; | ||
| 63 | |||
| 64 | diff --git a/src/internal/safefilepath/path.go b/src/internal/safefilepath/path.go | ||
| 65 | new file mode 100644 | ||
| 66 | index 0000000..0f0a270 | ||
| 67 | --- /dev/null | ||
| 68 | +++ b/src/internal/safefilepath/path.go | ||
| 69 | @@ -0,0 +1,21 @@ | ||
| 70 | +// Copyright 2022 The Go Authors. All rights reserved. | ||
| 71 | +// Use of this source code is governed by a BSD-style | ||
| 72 | +// license that can be found in the LICENSE file. | ||
| 73 | + | ||
| 74 | +// Package safefilepath manipulates operating-system file paths. | ||
| 75 | +package safefilepath | ||
| 76 | + | ||
| 77 | +import ( | ||
| 78 | + "errors" | ||
| 79 | +) | ||
| 80 | + | ||
| 81 | +var errInvalidPath = errors.New("invalid path") | ||
| 82 | + | ||
| 83 | +// FromFS converts a slash-separated path into an operating-system path. | ||
| 84 | +// | ||
| 85 | +// FromFS returns an error if the path cannot be represented by the operating | ||
| 86 | +// system. For example, paths containing '\' and ':' characters are rejected | ||
| 87 | +// on Windows. | ||
| 88 | +func FromFS(path string) (string, error) { | ||
| 89 | + return fromFS(path) | ||
| 90 | +} | ||
| 91 | diff --git a/src/internal/safefilepath/path_other.go b/src/internal/safefilepath/path_other.go | ||
| 92 | new file mode 100644 | ||
| 93 | index 0000000..f93da18 | ||
| 94 | --- /dev/null | ||
| 95 | +++ b/src/internal/safefilepath/path_other.go | ||
| 96 | @@ -0,0 +1,23 @@ | ||
| 97 | +// Copyright 2022 The Go Authors. All rights reserved. | ||
| 98 | +// Use of this source code is governed by a BSD-style | ||
| 99 | +// license that can be found in the LICENSE file. | ||
| 100 | + | ||
| 101 | +//go:build !windows | ||
| 102 | + | ||
| 103 | +package safefilepath | ||
| 104 | + | ||
| 105 | +import "runtime" | ||
| 106 | + | ||
| 107 | +func fromFS(path string) (string, error) { | ||
| 108 | + if runtime.GOOS == "plan9" { | ||
| 109 | + if len(path) > 0 && path[0] == '#' { | ||
| 110 | + return path, errInvalidPath | ||
| 111 | + } | ||
| 112 | + } | ||
| 113 | + for i := range path { | ||
| 114 | + if path[i] == 0 { | ||
| 115 | + return "", errInvalidPath | ||
| 116 | + } | ||
| 117 | + } | ||
| 118 | + return path, nil | ||
| 119 | +} | ||
| 120 | diff --git a/src/internal/safefilepath/path_test.go b/src/internal/safefilepath/path_test.go | ||
| 121 | new file mode 100644 | ||
| 122 | index 0000000..dc662c1 | ||
| 123 | --- /dev/null | ||
| 124 | +++ b/src/internal/safefilepath/path_test.go | ||
| 125 | @@ -0,0 +1,88 @@ | ||
| 126 | +// Copyright 2022 The Go Authors. All rights reserved. | ||
| 127 | +// Use of this source code is governed by a BSD-style | ||
| 128 | +// license that can be found in the LICENSE file. | ||
| 129 | + | ||
| 130 | +package safefilepath_test | ||
| 131 | + | ||
| 132 | +import ( | ||
| 133 | + "internal/safefilepath" | ||
| 134 | + "os" | ||
| 135 | + "path/filepath" | ||
| 136 | + "runtime" | ||
| 137 | + "testing" | ||
| 138 | +) | ||
| 139 | + | ||
| 140 | +type PathTest struct { | ||
| 141 | + path, result string | ||
| 142 | +} | ||
| 143 | + | ||
| 144 | +const invalid = "" | ||
| 145 | + | ||
| 146 | +var fspathtests = []PathTest{ | ||
| 147 | + {".", "."}, | ||
| 148 | + {"/a/b/c", "/a/b/c"}, | ||
| 149 | + {"a\x00b", invalid}, | ||
| 150 | +} | ||
| 151 | + | ||
| 152 | +var winreservedpathtests = []PathTest{ | ||
| 153 | + {`a\b`, `a\b`}, | ||
| 154 | + {`a:b`, `a:b`}, | ||
| 155 | + {`a/b:c`, `a/b:c`}, | ||
| 156 | + {`NUL`, `NUL`}, | ||
| 157 | + {`./com1`, `./com1`}, | ||
| 158 | + {`a/nul/b`, `a/nul/b`}, | ||
| 159 | +} | ||
| 160 | + | ||
| 161 | +// Whether a reserved name with an extension is reserved or not varies by | ||
| 162 | +// Windows version. | ||
| 163 | +var winreservedextpathtests = []PathTest{ | ||
| 164 | + {"nul.txt", "nul.txt"}, | ||
| 165 | + {"a/nul.txt/b", "a/nul.txt/b"}, | ||
| 166 | +} | ||
| 167 | + | ||
| 168 | +var plan9reservedpathtests = []PathTest{ | ||
| 169 | + {`#c`, `#c`}, | ||
| 170 | +} | ||
| 171 | + | ||
| 172 | +func TestFromFS(t *testing.T) { | ||
| 173 | + switch runtime.GOOS { | ||
| 174 | + case "windows": | ||
| 175 | + if canWriteFile(t, "NUL") { | ||
| 176 | + t.Errorf("can unexpectedly write a file named NUL on Windows") | ||
| 177 | + } | ||
| 178 | + if canWriteFile(t, "nul.txt") { | ||
| 179 | + fspathtests = append(fspathtests, winreservedextpathtests...) | ||
| 180 | + } else { | ||
| 181 | + winreservedpathtests = append(winreservedpathtests, winreservedextpathtests...) | ||
| 182 | + } | ||
| 183 | + for i := range winreservedpathtests { | ||
| 184 | + winreservedpathtests[i].result = invalid | ||
| 185 | + } | ||
| 186 | + for i := range fspathtests { | ||
| 187 | + fspathtests[i].result = filepath.FromSlash(fspathtests[i].result) | ||
| 188 | + } | ||
| 189 | + case "plan9": | ||
| 190 | + for i := range plan9reservedpathtests { | ||
| 191 | + plan9reservedpathtests[i].result = invalid | ||
| 192 | + } | ||
| 193 | + } | ||
| 194 | + tests := fspathtests | ||
| 195 | + tests = append(tests, winreservedpathtests...) | ||
| 196 | + tests = append(tests, plan9reservedpathtests...) | ||
| 197 | + for _, test := range tests { | ||
| 198 | + got, err := safefilepath.FromFS(test.path) | ||
| 199 | + if (got == "") != (err != nil) { | ||
| 200 | + t.Errorf(`FromFS(%q) = %q, %v; want "" only if err != nil`, test.path, got, err) | ||
| 201 | + } | ||
| 202 | + if got != test.result { | ||
| 203 | + t.Errorf("FromFS(%q) = %q, %v; want %q", test.path, got, err, test.result) | ||
| 204 | + } | ||
| 205 | + } | ||
| 206 | +} | ||
| 207 | + | ||
| 208 | +func canWriteFile(t *testing.T, name string) bool { | ||
| 209 | + path := filepath.Join(t.TempDir(), name) | ||
| 210 | + os.WriteFile(path, []byte("ok"), 0666) | ||
| 211 | + b, _ := os.ReadFile(path) | ||
| 212 | + return string(b) == "ok" | ||
| 213 | +} | ||
| 214 | diff --git a/src/internal/safefilepath/path_windows.go b/src/internal/safefilepath/path_windows.go | ||
| 215 | new file mode 100644 | ||
| 216 | index 0000000..909c150 | ||
| 217 | --- /dev/null | ||
| 218 | +++ b/src/internal/safefilepath/path_windows.go | ||
| 219 | @@ -0,0 +1,95 @@ | ||
| 220 | +// Copyright 2022 The Go Authors. All rights reserved. | ||
| 221 | +// Use of this source code is governed by a BSD-style | ||
| 222 | +// license that can be found in the LICENSE file. | ||
| 223 | + | ||
| 224 | +package safefilepath | ||
| 225 | + | ||
| 226 | +import ( | ||
| 227 | + "syscall" | ||
| 228 | + "unicode/utf8" | ||
| 229 | +) | ||
| 230 | + | ||
| 231 | +func fromFS(path string) (string, error) { | ||
| 232 | + if !utf8.ValidString(path) { | ||
| 233 | + return "", errInvalidPath | ||
| 234 | + } | ||
| 235 | + for len(path) > 1 && path[0] == '/' && path[1] == '/' { | ||
| 236 | + path = path[1:] | ||
| 237 | + } | ||
| 238 | + containsSlash := false | ||
| 239 | + for p := path; p != ""; { | ||
| 240 | + // Find the next path element. | ||
| 241 | + i := 0 | ||
| 242 | + dot := -1 | ||
| 243 | + for i < len(p) && p[i] != '/' { | ||
| 244 | + switch p[i] { | ||
| 245 | + case 0, '\\', ':': | ||
| 246 | + return "", errInvalidPath | ||
| 247 | + case '.': | ||
| 248 | + if dot < 0 { | ||
| 249 | + dot = i | ||
| 250 | + } | ||
| 251 | + } | ||
| 252 | + i++ | ||
| 253 | + } | ||
| 254 | + part := p[:i] | ||
| 255 | + if i < len(p) { | ||
| 256 | + containsSlash = true | ||
| 257 | + p = p[i+1:] | ||
| 258 | + } else { | ||
| 259 | + p = "" | ||
| 260 | + } | ||
| 261 | + // Trim the extension and look for a reserved name. | ||
| 262 | + base := part | ||
| 263 | + if dot >= 0 { | ||
| 264 | + base = part[:dot] | ||
| 265 | + } | ||
| 266 | + if isReservedName(base) { | ||
| 267 | + if dot < 0 { | ||
| 268 | + return "", errInvalidPath | ||
| 269 | + } | ||
| 270 | + // The path element is a reserved name with an extension. | ||
| 271 | + // Some Windows versions consider this a reserved name, | ||
| 272 | + // while others do not. Use FullPath to see if the name is | ||
| 273 | + // reserved. | ||
| 274 | + if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` { | ||
| 275 | + return "", errInvalidPath | ||
| 276 | + } | ||
| 277 | + } | ||
| 278 | + } | ||
| 279 | + if containsSlash { | ||
| 280 | + // We can't depend on strings, so substitute \ for / manually. | ||
| 281 | + buf := []byte(path) | ||
| 282 | + for i, b := range buf { | ||
| 283 | + if b == '/' { | ||
| 284 | + buf[i] = '\\' | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + path = string(buf) | ||
| 288 | + } | ||
| 289 | + return path, nil | ||
| 290 | +} | ||
| 291 | + | ||
| 292 | +// isReservedName reports if name is a Windows reserved device name. | ||
| 293 | +// It does not detect names with an extension, which are also reserved on some Windows versions. | ||
| 294 | +// | ||
| 295 | +// For details, search for PRN in | ||
| 296 | +// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file. | ||
| 297 | +func isReservedName(name string) bool { | ||
| 298 | + if 3 <= len(name) && len(name) <= 4 { | ||
| 299 | + switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) { | ||
| 300 | + case "CON", "PRN", "AUX", "NUL": | ||
| 301 | + return len(name) == 3 | ||
| 302 | + case "COM", "LPT": | ||
| 303 | + return len(name) == 4 && '1' <= name[3] && name[3] <= '9' | ||
| 304 | + } | ||
| 305 | + } | ||
| 306 | + return false | ||
| 307 | +} | ||
| 308 | + | ||
| 309 | +func toUpper(c byte) byte { | ||
| 310 | + if 'a' <= c && c <= 'z' { | ||
| 311 | + return c - ('a' - 'A') | ||
| 312 | + } | ||
| 313 | + return c | ||
| 314 | +} | ||
| 315 | diff --git a/src/net/http/fs.go b/src/net/http/fs.go | ||
| 316 | index 57e731e..43ee4b5 100644 | ||
| 317 | --- a/src/net/http/fs.go | ||
| 318 | +++ b/src/net/http/fs.go | ||
| 319 | @@ -9,6 +9,7 @@ package http | ||
| 320 | import ( | ||
| 321 | "errors" | ||
| 322 | "fmt" | ||
| 323 | + "internal/safefilepath" | ||
| 324 | "io" | ||
| 325 | "io/fs" | ||
| 326 | "mime" | ||
| 327 | @@ -69,14 +70,15 @@ func mapDirOpenError(originalErr error, name string) error { | ||
| 328 | // Open implements FileSystem using os.Open, opening files for reading rooted | ||
| 329 | // and relative to the directory d. | ||
| 330 | func (d Dir) Open(name string) (File, error) { | ||
| 331 | - if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { | ||
| 332 | - return nil, errors.New("http: invalid character in file path") | ||
| 333 | + path, err := safefilepath.FromFS(path.Clean("/" + name)) | ||
| 334 | + if err != nil { | ||
| 335 | + return nil, errors.New("http: invalid or unsafe file path") | ||
| 336 | } | ||
| 337 | dir := string(d) | ||
| 338 | if dir == "" { | ||
| 339 | dir = "." | ||
| 340 | } | ||
| 341 | - fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))) | ||
| 342 | + fullName := filepath.Join(dir, path) | ||
| 343 | f, err := os.Open(fullName) | ||
| 344 | if err != nil { | ||
| 345 | return nil, mapDirOpenError(err, fullName) | ||
| 346 | diff --git a/src/net/http/fs_test.go b/src/net/http/fs_test.go | ||
| 347 | index b42ade1..941448a 100644 | ||
| 348 | --- a/src/net/http/fs_test.go | ||
| 349 | +++ b/src/net/http/fs_test.go | ||
| 350 | @@ -648,6 +648,34 @@ func TestFileServerZeroByte(t *testing.T) { | ||
| 351 | } | ||
| 352 | } | ||
| 353 | |||
| 354 | +func TestFileServerNamesEscape(t *testing.T) { | ||
| 355 | + t.Run("h1", func(t *testing.T) { | ||
| 356 | + testFileServerNamesEscape(t, h1Mode) | ||
| 357 | + }) | ||
| 358 | + t.Run("h2", func(t *testing.T) { | ||
| 359 | + testFileServerNamesEscape(t, h2Mode) | ||
| 360 | + }) | ||
| 361 | +} | ||
| 362 | +func testFileServerNamesEscape(t *testing.T, h2 bool) { | ||
| 363 | + defer afterTest(t) | ||
| 364 | + ts := newClientServerTest(t, h2, FileServer(Dir("testdata"))).ts | ||
| 365 | + defer ts.Close() | ||
| 366 | + for _, path := range []string{ | ||
| 367 | + "/../testdata/file", | ||
| 368 | + "/NUL", // don't read from device files on Windows | ||
| 369 | + } { | ||
| 370 | + res, err := ts.Client().Get(ts.URL + path) | ||
| 371 | + if err != nil { | ||
| 372 | + t.Fatal(err) | ||
| 373 | + } | ||
| 374 | + res.Body.Close() | ||
| 375 | + if res.StatusCode < 400 || res.StatusCode > 599 { | ||
| 376 | + t.Errorf("Get(%q): got status %v, want 4xx or 5xx", path, res.StatusCode) | ||
| 377 | + } | ||
| 378 | + | ||
| 379 | + } | ||
| 380 | +} | ||
| 381 | + | ||
| 382 | type fakeFileInfo struct { | ||
| 383 | dir bool | ||
| 384 | basename string | ||
| 385 | diff --git a/src/os/file.go b/src/os/file.go | ||
| 386 | index e717f17..cb87158 100644 | ||
| 387 | --- a/src/os/file.go | ||
| 388 | +++ b/src/os/file.go | ||
| 389 | @@ -37,12 +37,12 @@ | ||
| 390 | // Note: The maximum number of concurrent operations on a File may be limited by | ||
| 391 | // the OS or the system. The number should be high, but exceeding it may degrade | ||
| 392 | // performance or cause other issues. | ||
| 393 | -// | ||
| 394 | package os | ||
| 395 | |||
| 396 | import ( | ||
| 397 | "errors" | ||
| 398 | "internal/poll" | ||
| 399 | + "internal/safefilepath" | ||
| 400 | "internal/testlog" | ||
| 401 | "internal/unsafeheader" | ||
| 402 | "io" | ||
| 403 | @@ -623,6 +623,8 @@ func isWindowsNulName(name string) bool { | ||
| 404 | // the /prefix tree, then using DirFS does not stop the access any more than using | ||
| 405 | // os.Open does. DirFS is therefore not a general substitute for a chroot-style security | ||
| 406 | // mechanism when the directory tree contains arbitrary content. | ||
| 407 | +// | ||
| 408 | +// The directory dir must not be "". | ||
| 409 | func DirFS(dir string) fs.FS { | ||
| 410 | return dirFS(dir) | ||
| 411 | } | ||
| 412 | @@ -641,10 +643,11 @@ func containsAny(s, chars string) bool { | ||
| 413 | type dirFS string | ||
| 414 | |||
| 415 | func (dir dirFS) Open(name string) (fs.File, error) { | ||
| 416 | - if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) { | ||
| 417 | - return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid} | ||
| 418 | + fullname, err := dir.join(name) | ||
| 419 | + if err != nil { | ||
| 420 | + return nil, &PathError{Op: "stat", Path: name, Err: err} | ||
| 421 | } | ||
| 422 | - f, err := Open(string(dir) + "/" + name) | ||
| 423 | + f, err := Open(fullname) | ||
| 424 | if err != nil { | ||
| 425 | return nil, err // nil fs.File | ||
| 426 | } | ||
| 427 | @@ -652,16 +655,35 @@ func (dir dirFS) Open(name string) (fs.File, error) { | ||
| 428 | } | ||
| 429 | |||
| 430 | func (dir dirFS) Stat(name string) (fs.FileInfo, error) { | ||
| 431 | - if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) { | ||
| 432 | - return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid} | ||
| 433 | + fullname, err := dir.join(name) | ||
| 434 | + if err != nil { | ||
| 435 | + return nil, &PathError{Op: "stat", Path: name, Err: err} | ||
| 436 | } | ||
| 437 | - f, err := Stat(string(dir) + "/" + name) | ||
| 438 | + f, err := Stat(fullname) | ||
| 439 | if err != nil { | ||
| 440 | return nil, err | ||
| 441 | } | ||
| 442 | return f, nil | ||
| 443 | } | ||
| 444 | |||
| 445 | +// join returns the path for name in dir. | ||
| 446 | +func (dir dirFS) join(name string) (string, error) { | ||
| 447 | + if dir == "" { | ||
| 448 | + return "", errors.New("os: DirFS with empty root") | ||
| 449 | + } | ||
| 450 | + if !fs.ValidPath(name) { | ||
| 451 | + return "", ErrInvalid | ||
| 452 | + } | ||
| 453 | + name, err := safefilepath.FromFS(name) | ||
| 454 | + if err != nil { | ||
| 455 | + return "", ErrInvalid | ||
| 456 | + } | ||
| 457 | + if IsPathSeparator(dir[len(dir)-1]) { | ||
| 458 | + return string(dir) + name, nil | ||
| 459 | + } | ||
| 460 | + return string(dir) + string(PathSeparator) + name, nil | ||
| 461 | +} | ||
| 462 | + | ||
| 463 | // ReadFile reads the named file and returns the contents. | ||
| 464 | // A successful call returns err == nil, not err == EOF. | ||
| 465 | // Because ReadFile reads the whole file, it does not treat an EOF from Read | ||
| 466 | diff --git a/src/os/os_test.go b/src/os/os_test.go | ||
| 467 | index 506f1fb..be269bb 100644 | ||
| 468 | --- a/src/os/os_test.go | ||
| 469 | +++ b/src/os/os_test.go | ||
| 470 | @@ -2702,6 +2702,44 @@ func TestDirFS(t *testing.T) { | ||
| 471 | if err == nil { | ||
| 472 | t.Fatalf(`Open testdata\dirfs succeeded`) | ||
| 473 | } | ||
| 474 | + | ||
| 475 | + // Test that Open does not open Windows device files. | ||
| 476 | + _, err = d.Open(`NUL`) | ||
| 477 | + if err == nil { | ||
| 478 | + t.Errorf(`Open NUL succeeded`) | ||
| 479 | + } | ||
| 480 | +} | ||
| 481 | + | ||
| 482 | +func TestDirFSRootDir(t *testing.T) { | ||
| 483 | + cwd, err := os.Getwd() | ||
| 484 | + if err != nil { | ||
| 485 | + t.Fatal(err) | ||
| 486 | + } | ||
| 487 | + cwd = cwd[len(filepath.VolumeName(cwd)):] // trim volume prefix (C:) on Windows | ||
| 488 | + cwd = filepath.ToSlash(cwd) // convert \ to / | ||
| 489 | + cwd = strings.TrimPrefix(cwd, "/") // trim leading / | ||
| 490 | + | ||
| 491 | + // Test that Open can open a path starting at /. | ||
| 492 | + d := DirFS("/") | ||
| 493 | + f, err := d.Open(cwd + "/testdata/dirfs/a") | ||
| 494 | + if err != nil { | ||
| 495 | + t.Fatal(err) | ||
| 496 | + } | ||
| 497 | + f.Close() | ||
| 498 | +} | ||
| 499 | + | ||
| 500 | +func TestDirFSEmptyDir(t *testing.T) { | ||
| 501 | + d := DirFS("") | ||
| 502 | + cwd, _ := os.Getwd() | ||
| 503 | + for _, path := range []string{ | ||
| 504 | + "testdata/dirfs/a", // not DirFS(".") | ||
| 505 | + filepath.ToSlash(cwd) + "/testdata/dirfs/a", // not DirFS("/") | ||
| 506 | + } { | ||
| 507 | + _, err := d.Open(path) | ||
| 508 | + if err == nil { | ||
| 509 | + t.Fatalf(`DirFS("").Open(%q) succeeded`, path) | ||
| 510 | + } | ||
| 511 | + } | ||
| 512 | } | ||
| 513 | |||
| 514 | func TestDirFSPathsValid(t *testing.T) { | ||
