Skip to content

blob/fileblob: path traversal via ".." key escapes bucket root (arbitrary file read/write/delete) #3667

@Ryujiyasu

Description

@Ryujiyasu

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.gopath() (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 unchanged

ListPaged() — 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 directory

Attack 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions