How croc Handles Symlinks and File Permissions in Secure File Transfers
croc treats symlinks and file-system permissions as first-class metadata during transfers, computing hashes from symlink targets rather than the links themselves and restoring original permission bits with os.Chmod after writing files.
croc is a secure, cross-platform file transfer tool written in Go that prioritizes data integrity and security. When transferring directories containing symbolic links or executable scripts, preserving metadata becomes critical. According to the schollz/croc source code, the implementation handles these edge cases through specialized hashing logic, path traversal validation, and permission restoration mechanisms.
Detecting Symlinks During Pre-Transfer Hashing
When croc indexes files for transfer, the utils.HashFile function in src/utils/utils.go (lines 102-121) uses os.Lstat to examine file metadata. If the file mode indicates os.ModeSymlink, the function computes the hash from the target path string rather than the link itself.
This approach ensures that identical symlink configurations produce identical hashes across different machines, even when absolute paths differ. The target path is read using standard library calls, and that string content becomes the input for the hashing algorithm.
// Hashing logic in src/utils/utils.go
hash, err := utils.HashFile(path, "md5")
// Returns SHA256 of the target path if path is a symlink
Validating Symlink Targets on the Receiver
The receiver implements strict validation for any symlink announced by the sender. Located in src/croc/croc.go, two critical functions enforce security boundaries to prevent path traversal and symlink hijacking attacks.
Preventing Path Traversal with validateReceiveSymlinkTarget
The validateReceiveSymlinkTarget function (lines 60-70) inspects the target string announced by the sender. It mandates that the target must be a relative path that resolves inside the receive directory. Absolute paths are rejected, and any target attempting to escape the destination folder (using .. sequences) triggers an error, blocking the transfer.
// Validation in src/croc/croc.go
if err := validateReceiveSymlinkTarget(parentDir, fi.Symlink); err != nil {
return err // rejects unsafe or absolute targets
}
Blocking Symlink Overwrites with rejectSymlinkDestination
The rejectSymlinkDestination function (lines 12-22) prevents the receiver from overwriting existing symlinks on the local filesystem. This protection stops a malicious sender from redirecting an existing symlink to sensitive files, such as SSH keys or password databases, effectively preventing symlink hijacking.
// Protection against overwriting existing links
if err := rejectSymlinkDestination(destPath); err != nil {
return err // prevents accidental symlink hijacking
}
Preserving File Permissions Across Systems
For each regular file, croc records the original os.FileMode during the initial scan. The FileInfo struct in src/models/models.go carries this metadata across the transfer. After writing the file content on the receiving side, the restore logic calls os.Chmod with the stored mode.
This step preserves executable bits, read-only flags, and custom permission octals (such as 0755 or 0644), ensuring that scripts remain executable and access controls remain intact after transfer.
// Restoring original permissions after writing
if err := os.Chmod(destPath, originalMode); err != nil {
log.Errorf("could not set permissions: %v", err)
}
Summary
- Symlink Detection: croc uses
os.Lstatinsrc/utils/utils.goto identify symlinks and hashes the target path rather than the link node. - Security Validation: The receiver validates symlink targets using
validateReceiveSymlinkTargetandrejectSymlinkDestinationinsrc/croc/croc.goto prevent path traversal and overwrites. - Metadata Preservation: Original
os.FileModepermissions are transmitted and restored viaos.Chmod, maintaining executable and read-only bits across different operating systems.
Frequently Asked Questions
Does croc transfer the content a symlink points to or just the link itself?
croc transfers only the symlink metadata—the target path string—not the content of the target file. The FileInfo struct carries the symlink target path, and the receiver creates a new symlink pointing to that validated path.
How does croc prevent symlink-based path traversal attacks?
The validateReceiveSymlinkTarget function ensures all symlink targets are relative paths that resolve within the receive directory. It rejects absolute paths and blocks attempts to traverse outside the destination folder using .. components, effectively preventing directory traversal attacks.
Are file permissions preserved when transferring between Windows and Linux?
croc captures and transmits the os.FileMode bits during transfer. While permission models differ between Windows and Linux, croc attempts to apply the equivalent permissions on the receiving side using os.Chmod. Executable bits and read-only flags are preserved when the underlying filesystem supports them.
What happens if a symlink already exists at the destination path?
The rejectSymlinkDestination function in src/croc/croc.go checks for existing symlinks at the destination and returns an error if one is found. This prevents a malicious sender from hijacking an existing symlink to point to sensitive files on the receiver's system.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →