This document describes logtap's security model and threat mitigations.
| Version | Supported |
|---|---|
| 0.4.x | ✅ |
| < 0.4 | ❌ |
logtap exposes log file access over HTTP. The primary security concern is path traversal attacks where an attacker attempts to read files outside the configured log directory.
Attacker goal: Read arbitrary files on the server (e.g., /etc/passwd, ~/.ssh/id_rsa)
Attack vectors:
- Directory traversal:
../../etc/passwd - Absolute paths:
/etc/passwd - Mixed separators:
..\\..\\etc\\passwd - Symlink escape: Create
logs/link -> /etc, requestlink/passwd - Null byte injection:
file.log\x00../../etc/passwd - Path prefix collision: base
/var/log, request resolves to/var/logs/evil - URL encoding:
%2e%2e%2f(decoded../)
All file access goes through resolve_safe_path() in src/logtap/core/validation.py:
1. Input validation
- Reject empty filenames
- Reject NUL bytes (\x00)
- Reject control characters (0x00-0x1F, 0x7F)
- Reject path traversal sequences (..)
- Reject path separators (/ and \)
- Reject absolute paths (Unix: /..., Windows: C:\...)
2. Path construction
- base_resolved = os.path.realpath(base_dir)
- filepath = os.path.join(base_resolved, filename)
- filepath_resolved = os.path.realpath(filepath)
3. Containment check
- common = os.path.commonpath([base_resolved, filepath_resolved])
- Verify: common == base_resolved
- This handles symlink escape and prefix collisions
4. File type check (optional)
- Verify target is a regular file (not directory, device, etc.)
startswith is not reliable for path containment:
# WRONG - prefix collision
base = "/var/log"
candidate = "/var/logs/evil"
candidate.startswith(base) # True! But /var/logs is different directorycommonpath correctly handles this:
# CORRECT
os.path.commonpath(["/var/log", "/var/logs/evil"]) # Returns "/var"
# "/var" != "/var/log" -> rejectedos.path.realpath() resolves all symlinks before the containment check:
base_dir/
ok.log
escape -> /etc/ (symlink to /etc)
Request: "escape/passwd"
1. Join: /base_dir/escape/passwd
2. Resolve: /etc/passwd (symlink followed)
3. Containment: commonpath(["/base_dir", "/etc/passwd"]) = "/"
4. "/" != "/base_dir" -> REJECTED
The resolve_safe_path function guarantees:
- Return value is always
Noneor an absolute canonical path - If non-None, the path is within
base_dir(symlink-safe) - If
require_exists=True, the path exists and is a regular file - No user input can escape the base directory
All HTTP endpoints that access files use resolve_safe_path:
| Endpoint | Handler |
|---|---|
GET /logs |
get_filepath() -> resolve_safe_path() |
GET /logs/multi |
resolve_safe_path() |
WS /logs/stream |
resolve_safe_path() |
GET /logs/sse |
get_filepath() -> resolve_safe_path() |
GET /parsed |
resolve_safe_path() |
| Input | Rejection Reason |
|---|---|
. |
Special directory entry |
.. |
Special directory entry |
../etc/passwd |
Contains path separator |
/etc/passwd |
Absolute path |
foo/bar |
Contains path separator |
foo\bar |
Contains path separator |
file\x00.log |
Contains NUL byte |
file\x1f.log |
Contains control char |
C:\Windows |
Windows drive letter |
| (empty string) | Empty filename |
| Input | Notes |
|---|---|
my..log |
Allowed - ".." not a path component |
v1..bak |
Allowed - ".." not a path component |
file.log |
Normal filename |
Optional API key authentication via X-API-Key header or api_key query parameter.
logtap collect --api-key secret
logtap tail run1 --api-key secretUses google-re2 for regex operations, which guarantees linear-time matching and prevents ReDoS attacks.
- Search term: max 100 characters
- Lines per request: max 1000
- Multi-file query: max 10 files
We take the security of logtap seriously. If you find a vulnerability:
- Do not open a public GitHub issue
- Send a direct message to the project maintainers or use GitHub's private vulnerability reporting
- Provide detailed information about the vulnerability
- Allow time for us to address it before public disclosure
We appreciate your help in keeping logtap and its users secure!