-
Notifications
You must be signed in to change notification settings - Fork 833
Description
Summary
blob/fileblob's path() function uses filepath.Join(b.dir, escapeKey(key)) to resolve blob keys to filesystem paths, but escapeKey() only escapes ../ (the slash after ..), not .. as a standalone key. This allows an attacker-controlled blob key to escape the bucket root directory.
Affected Code
blob/fileblob/fileblob.go — path() (no containment check):
func (b *bucket) path(key string) (string, error) {
path := filepath.Join(b.dir, escapeKey(key)) // escapeKey misses ".." alone
if strings.HasSuffix(path, attrsExt) {
return "", errAttrsExt
}
return path, nil // no HasPrefix(path, b.dir) check
}escapeKey() — only escapes ../, not standalone ..:
// only triggers when a slash follows "..":
case i > 1 && c == '/' && r[i-1] == '.' && r[i-2] == '.':
return true // the ".." alone passes through unchangedListPaged() — unescaped prefix used as walk root:
root := b.dir
if i := strings.LastIndex(opts.Prefix, "/"); i > -1 {
root = filepath.Join(root, opts.Prefix[:i]) // prefix[:i] not validated
}Proof
escapeKey("..") = ".." // not escaped
filepath.Join("/data/bucket", "..") = "/data" // ← escapes root!
// ListPaged with Prefix "../etc":
// LastIndex("../etc", "/") = 2; prefix[:2] = ".."
// root = filepath.Join("/data/bucket", "..") = "/data"
// WalkDir traverses /data — entire parent directoryAttack Paths
| Operation | Key/Prefix | Effect |
|---|---|---|
List |
Prefix "../" |
WalkDir starts at parent of bucket — full directory listing |
NewReader |
"../../etc/passwd" |
Reads arbitrary file outside bucket |
NewWriter |
"../other-bucket/file" |
Overwrites file in adjacent directory |
Delete |
".." |
os.Remove(parent_of_bucket) — deletes parent if empty |
Impact
Any Go application using gocloud.dev/blob with the file:// URL scheme (which uses fileblob internally) is affected when blob keys are user-controlled. This includes development environments, on-premise deployments, and production systems using local filesystem as blob backend.
Suggested Fix
Add a containment check after computing the path in path():
func (b *bucket) path(key string) (string, error) {
p := filepath.Join(b.dir, escapeKey(key))
if !strings.HasPrefix(filepath.Clean(p)+string(os.PathSeparator),
filepath.Clean(b.dir)+string(os.PathSeparator)) {
return "", fmt.Errorf("fileblob: key %q escapes bucket root", key)
}
if strings.HasSuffix(p, attrsExt) {
return "", errAttrsExt
}
return p, nil
}Similarly, ListPaged should validate that the computed walk root remains within b.dir.
Note: Issue #2955 proposed adopting RESOLVE_BENEATH (Linux openat2 syscall) for defence-in-depth — that would also address this class of traversal at the OS level.