Skip to content

Conversation

@mmartinv
Copy link

@mmartinv mmartinv commented Oct 21, 2025

Replace os.Rename with os.OpenRoot to prevent path traversal attacks in
the upload finalize function. The previous implementation was vulnerable
to malicious filenames that could escape the intended upload directory
using path traversal sequences (e.g., ../../etc/passwd).

The refactored implementation:

  • Uses os.OpenRoot to create a sandboxed filesystem rooted at u.Dir
  • Detects same filesystem using syscall.Stat device ID comparison
  • Uses os.Rename when files are on the same filesystem for efficiency
  • Falls back to copy when files are on different filesystems
  • Backs up existing destination files with timestamp suffix before overwriting
  • Copies files through the rooted filesystem, which constrains all
    operations within the upload directory
  • Prevents symbolic links from referencing locations outside the root
  • Ensures u.Rename cannot escape u.Dir even with path traversal attempts

Closes #69

@mmartinv mmartinv force-pushed the fsim-upload-chroot-security branch 2 times, most recently from acd3205 to c49848a Compare October 21, 2025 11:22
Copy link
Member

@runcom runcom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@runcom
Copy link
Member

runcom commented Oct 21, 2025

@ben-krieger ptal

@ben-krieger
Copy link
Member

Is CreateTemp still needed? It was initially introduced, because os.Rename didn't work across filesystems (e.g. tmpfs to ext4).

@mmartinv
Copy link
Author

Is CreateTemp still needed? It was initially introduced, because os.Rename didn't work across filesystems (e.g. tmpfs to ext4).

The proposed changes always copy the contents from the temp file to the final destination so it will work across filesystems. However, always copying the contents from the temp file to the final destination is sub-optimal for temp files created in the same filesystem. I like the idea of creating temporary files to upload the content and only create the destination file if the upload was successful so I think the better approach here is to maintain the CreateTemp and detect if the temporary file is in the same filesystem as the destination file and if it is, call rename instead of copy. I will update the PR with the appropriate changes.

@mmartinv mmartinv force-pushed the fsim-upload-chroot-security branch from c49848a to 0fb3899 Compare October 23, 2025 11:16
@mmartinv mmartinv requested a review from runcom October 23, 2025 11:18
@mmartinv mmartinv force-pushed the fsim-upload-chroot-security branch 2 times, most recently from c1c38c8 to a7e2026 Compare October 23, 2025 12:10
@runcom
Copy link
Member

runcom commented Oct 30, 2025

@mmartinv can you rebase?

@ben-krieger can you take a look?

Replace os.Rename with os.OpenRoot to prevent path traversal attacks in
the upload finalize function. The previous implementation was vulnerable
to malicious filenames that could escape the intended upload directory
using path traversal sequences (e.g., ../../etc/passwd).

The refactored implementation:
- Uses os.OpenRoot to create a sandboxed filesystem rooted at u.Dir
- Detects same filesystem using syscall.Stat device ID comparison
- Uses os.Rename when files are on the same filesystem for efficiency
- Falls back to copy when files are on different filesystems
- Backs up existing destination files with timestamp suffix before overwriting
- Copies files through the rooted filesystem, which constrains all
  operations within the upload directory
- Prevents symbolic links from referencing locations outside the root
- Ensures u.Rename cannot escape u.Dir even with path traversal attempts

Closes fido-device-onboard#69

Signed-off-by: Miguel Martín <[email protected]>
Add unit tests to verify the os.OpenRoot-based security implementation
in the fdo.upload owner module:

- TestUploadRequestPathTraversalPrevention: Tests multiple path traversal
  attack vectors including ../, absolute paths, and embedded traversal
  sequences to ensure os.OpenRoot prevents escaping the upload directory

- TestUploadRequestNormalFlow: Validates normal upload operation with
  proper file placement and content verification

- TestUploadRequestDefaultRename: Ensures empty Rename field defaults
  to basename of Name parameter

- TestUploadRequestSHA384Mismatch: Verifies uploads fail when SHA-384
  checksum doesn't match

- TestUploadRequestBackupExistingFile: Tests that existing files are
  backed up with timestamp suffixes before being overwritten

- TestUploadRequestSameFilesystemRename: Verifies rename optimization
  when temp file and destination are on the same filesystem

- TestUploadRequestBackupWithoutExtension: Tests backup of files without
  extensions to ensure timestamp formatting works correctly

All tests pass and confirm the security fix and optimizations from the
previous commit effectively prevent path traversal vulnerabilities while
providing efficient file operations.

Related to fido-device-onboard#69

Signed-off-by: Miguel Martín <[email protected]>
@mmartinv mmartinv force-pushed the fsim-upload-chroot-security branch from a7e2026 to 904702b Compare October 30, 2025 11:23
func sameFilesystem(path1, path2 string) (bool, error) {
var stat1, stat2 syscall.Stat_t

if err := syscall.Stat(path1, &stat1); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're using syscall.Stat because you want to compare the Dev (uint64) field. However, this is not portable and will break Windows compatibility, at the least.

GOOS=windows go doc syscall Stat_t
doc: no symbol Stat_t in packages syscall

Please move sameFilesystem to fsim/fs_unix.go with the appropriate build flags for Unix-like OSes that have a syscall.Stat function and then create an implementation that always returns false (I guess?) under fsim/fs_other.go.

Comment on lines +142 to +143
if err != nil {
// File doesn't exist, nothing to backup
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should only return nil if errors.Is(err, fs.ErrNotExist) and return err if err != nil otherwise.

if err = os.Rename(tempFilePath, destPath); err != nil {
return false, false, fmt.Errorf("error renaming temp file to %q: %w", u.Rename, err)
}
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skip the else and make sure the if always returns. This is preferred Go style.

// Construct the actual destination path
destPath := filepath.Join(u.Dir, u.Rename)

// Verify the resolved path is still within u.Dir (security check)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this done rather than using rootDir from os.OpenRoot?

return false, false, fmt.Errorf("path traversal detected in rename: %q", u.Rename)
}

// Backup existing file if present
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is potentially nice behavior not to clobber the file, but it's also unconfigurable and undocumented.

Following the principle of least surprise, I think you either overwrite or fail, not backup-and-overwrite.

I believe that the best API decision to reduce user error would be fail-if-existing with an optional configurable function for what to do on conflict.

Another clear API win would be to change Rename from a string to a func(string) string so that users can make every upload timestamped by upload time.

@@ -0,0 +1,576 @@
// SPDX-FileCopyrightText: (C) 2024 Intel Corporation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SPDX-FileCopyrightText: (C) 2024 Intel Corporation
// SPDX-FileCopyrightText: (C) 2025 Red Hat, Inc.

Thanks for adding so many tests! Take credit!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforce chroot-like security in fdo.upload owner FSIM using [os.Root](https://tip.golang.org/pkg/os#Root)

3 participants