hyperledger / hyperledger/fabric-x

bug(fxconfig/validation): symlink path traversal bypass in OSFileChecker and OSDirectoryChecker

Open
#242 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
64
Forks
80
Avg merge
1d 22h
Merged PRs (30d)
15

Description

## Summary

`OSFileChecker.Exists` and `OSDirectoryChecker.Exists` in `tools/fxconfig/internal/validation/validator.go` attempt to block path traversal by cleaning the path and rejecting inputs whose cleaned form contains `..`. However, the code then calls `os.Stat`, which follows symlinks. This means a symlink located inside an allowed-looking directory can resolve outside that directory and still pass validation.

In other words, lexical traversal such as `../../etc/passwd` is blocked, but symlink traversal is not.

## Affected File

`tools/fxconfig/internal/validation/validator.go`

## Affected Functions

- `OSFileChecker.Exists`
- `OSDirectoryChecker.Exists`

## Current Code Pattern

Both functions use this pattern:

```go
clean := filepath.Clean(path)
if strings.Contains(clean, "..") {
return errors.New("path traversal not allowed")
}
info, err := os.Stat(clean)
```

## Why This Is a Bug

`filepath.Clean` only normalizes lexical path components. It does not resolve symlinks.

`os.Stat` follows symlinks by default. So if a user provides a path such as:

```text
/safe/config/link.pem
```

and `link.pem` is a symlink to something outside the intended directory, for example:

```text
/etc/passwd
```

then:

- `filepath.Clean("/safe/config/link.pem")` stays `/safe/config/link.pem`
- the `..` check passes
- `os.Stat("/safe/config/link.pem")` follows the symlink
- validation succeeds even though the real target is outside the intended boundary

## Impact

This allows a symlink-based path traversal bypass.

Depending on how these validated paths are later used, this can lead to:

- reading files outside the expected config boundary
- loading certificates/keys/MSP material from unintended locations
- security issues caused by trusting a path that only appears safe lexically

## Reproduction Idea

A simple reproduction is:

1. Create a “safe” directory.
2. Create a file outside that directory.
3. Create a symlink inside the safe directory that points to the outside file.
4. Pass the symlink path to `OSFileChecker.Exists` or `OSDirectoryChecker.Exists`.

Expected secure behavior:
- the checker should reject the path because the resolved real path escapes the intended directory.

Actual behavior:
- the checker accepts it because `os.Stat` follows the symlink and there is no post-resolution boundary check.

## Suggested Fix

Resolve and validate the real path before accepting it.

A robust approach would be:

1. Use `os.Lstat` first to inspect the provided path without blindly following symlinks.
2. Resolve the final path with `filepath.EvalSymlinks`.
3. Enforce that the resolved real path stays under an allowlisted base directory.
4. Only then continue with the file/directory check.

Conceptually:

```go
clean := filepath.Clean(path)

if _, err := os.Lstat(clean); err != nil {
return err
}

real, err := filepath.EvalSymlinks(clean)
if err != nil {
return err
}

if !strings.HasPrefix(real, allowedBase) {
return errors.New("resolved path escapes allowed base")
}

info, err := os.Stat(real)
```

## Notes

A second `strings.Contains(real, "..")` check on its own would not be sufficient, because a resolved symlink target can escape the intended directory without containing any literal `..` segment. A real-path boundary check against an allowlisted base is the important part.

## Environment

- Repo: `hyperledger/fabric-x`
- File: `tools/fxconfig/internal/validation/validator.go`
- Branch checked: `main`

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.