Move `doc/adr/` to `adr/` for discoverability. Remove the generic ADR README — `ls adr/` serves the same purpose. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.4 KiB
ADR-001: Path Traversal Protection for Cheatsheet Names
Date: 2025-01-21
Status
Accepted
Context
The cheat tool allows users to create, edit, and remove cheatsheets using commands like:
cheat --edit <name>cheat --rm <name>
Without validation, a user could potentially provide malicious names like:
../../../etc/passwd(directory traversal)/etc/passwd(absolute path)~/.ssh/authorized_keys(home directory expansion)
While cheat is a local tool run by the user themselves (not a network service), path traversal could still lead to:
- Accidental file overwrites outside cheatsheet directories
- Confusion about where files are being created
- Potential security issues in shared environments
Decision
We implemented input validation for cheatsheet names to prevent directory traversal attacks. The validation rejects names that:
- Contain
..(parent directory references) - Are absolute paths (start with
/on Unix) - Start with
~(home directory expansion) - Are empty
- Start with
.(hidden files - these are not displayed by cheat)
The validation is performed at the application layer before any file operations occur.
Implementation Details
Validation Function
The validation is implemented in internal/cheatpath/validate.go:
func ValidateSheetName(name string) error {
// Reject empty names
if name == "" {
return fmt.Errorf("cheatsheet name cannot be empty")
}
// Reject names containing directory traversal
if strings.Contains(name, "..") {
return fmt.Errorf("cheatsheet name cannot contain '..'")
}
// Reject absolute paths
if filepath.IsAbs(name) {
return fmt.Errorf("cheatsheet name cannot be an absolute path")
}
// Reject names that start with ~ (home directory expansion)
if strings.HasPrefix(name, "~") {
return fmt.Errorf("cheatsheet name cannot start with '~'")
}
// Reject hidden files (files that start with a dot)
filename := filepath.Base(name)
if strings.HasPrefix(filename, ".") {
return fmt.Errorf("cheatsheet name cannot start with '.' (hidden files are not supported)")
}
return nil
}
Integration Points
The validation is called in:
cmd/cheat/cmd_edit.go- before creating or editing a cheatsheetcmd/cheat/cmd_remove.go- before removing a cheatsheet
Allowed Patterns
The following patterns are explicitly allowed:
- Simple names:
docker,git - Nested paths:
docker/compose,lang/go/slice - Current directory references:
./mysheet
Consequences
Positive
- Safety: Prevents accidental or intentional file operations outside cheatsheet directories
- Simplicity: Validation happens early, before any file operations
- User-friendly: Clear error messages explain why a name was rejected
- Performance: Minimal overhead - simple string checks
- Compatibility: Doesn't break existing valid cheatsheet names
Negative
- Limitation: Users cannot use
..in cheatsheet names even if legitimate - No symlink support: Cannot create cheatsheets through symlinks outside the cheatpath
Neutral
- Uses Go's
filepath.IsAbs()which handles platform differences (Windows vs Unix) - No attempt to resolve or canonicalize paths - validation is purely syntactic
Security Considerations
Threat Model
cheat is a local command-line tool, not a network service. The primary threats are:
- User error (accidentally overwriting important files)
- Malicious scripts that invoke
cheatwith crafted arguments - Shared system scenarios where cheatsheets might be shared
What This Protects Against
- Directory traversal using
../ - Absolute path access to system files
- Shell expansion of
~to home directory - Empty names that might cause unexpected behavior
- Hidden files that wouldn't be displayed anyway
What This Does NOT Protect Against
- Users with filesystem permissions can still directly edit any file
- Symbolic links within the cheatpath pointing outside
- Race conditions (TOCTOU) - though minimal risk for a local tool
- Malicious content within cheatsheets themselves
Testing
Comprehensive tests ensure the validation works correctly:
- Unit tests (
internal/cheatpath/validate_test.go) verify the validation logic - Integration tests verify the actual binary blocks malicious inputs
- No system files are accessed during testing - all tests use isolated directories
Example test cases:
# These are blocked:
cheat --edit "../../../etc/passwd"
cheat --edit "/etc/passwd"
cheat --edit "~/.ssh/config"
cheat --rm ".."
# These are allowed:
cheat --edit "docker"
cheat --edit "docker/compose"
cheat --edit "./local"
Alternative Approaches Considered
-
Path resolution and verification: Resolve the final path and check if it's within the cheatpath
- Rejected: More complex, potential race conditions, platform-specific edge cases
-
Chroot/sandbox: Run file operations in a restricted environment
- Rejected: Overkill for a local tool, platform compatibility issues
-
Filename allowlist: Only allow alphanumeric characters and specific symbols
- Rejected: Too restrictive, would break existing cheatsheets with valid special characters
References
- OWASP Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory
- Go filepath package documentation: https://pkg.go.dev/path/filepath