hyperledger / hyperledger/fabric-x
bug(fxconfig/validation): symlink path traversal bypass in OSFileChecker and OSDirectoryChecker
- 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.