T09 · Insecure Skill Coding Practices
- Location
dx-monitor.py:203- Finding
Predictable Temporary State File Allows Symlink-Based File Overwrite
- Content
View full analysis
Vulnerability Details
File Location:
dx-monitor.py:20anddx-monitor.py:203-214
Vulnerability Type: Unsafe temporary-file handling and symbolic-link following
Risk Level: Mediumpython STATE_FILE = "/tmp/dx-monitor-state.json"python def load_state() -> Dict: """Load previous state.""" try: with open(STATE_FILE) as f: return json.load(f) except: return {'last_spots': [], 'last_check': 0} def save_state(state: Dict): """Save state.""" with open(STATE_FILE, 'w') as f: json.dump(state, f, indent=2)Technical Analysis
The monitor stores state at the fixed, globally predictable path
/tmp/dx-monitor-state.json. The file is opened using ordinaryopen()operations without:- Verifying that the path is not a symbolic link.
- Verifying ownership or file type.
- Creating an owner-only state directory.
- Setting explicit restrictive permissions.
- Using exclusive or atomic file creation.
- Writing to a protected temporary file and atomically replacing the destination.
On systems where applicable symbolic-link protections are absent, disabled, or bypassable, another local user can create the expected path as a symbolic link before the monitor creates it. When
save_state()opens the path using write mode, Python follows the link and truncates the linked destination.The broad exception handler in
load_state()also conceals ownership, format, and access errors, making manipulated state appear equivalent to missing state and reducing visibility into an attack.Attack Path
- The attacker confirms that the victim runs
dx-monitor.py watch --new-only, potentially through the documented recurring cron configuration. - Before the victim creates the state file, the attacker creates
/tmp/dx-monitor-state.jsonas a symbolic link to a chosen file. - The chosen target must be writable by the account tha ...[truncated 1642 chars]
- Remediation
View remediation
Remediation Suggestions
- Replace the shared
/tmppath with an account-specific state directory, preferably:$XDG_STATE_HOME/ham-radio-dx/state.json, or~/.local/state/ham-radio-dx/state.json.
- Create the parent directory with permissions
0700. - Create state files with permissions
0600. - Reject symbolic links and non-regular files using
os.lstat()and, where supported,os.open()withO_NOFOLLOW. - Write to a temporary file in the same protected directory, flush and synchronize it, and then use
os.replace()for atomic publication. - Verify that any existing state file is owned by the current effective user.
- Catch specific exceptions and report unsafe ownership, file-type, and permission conditions rather than silently treating all errors as missing state.
- Correct the documentation inconsistency:
SKILL.mdstates both~/dx-monitor-state.jsonand/tmp/dx-monitor-state.json. Document only the hardened account-specific location.
A secure implementation should follow this pattern:
python import json import os import tempfile from pathlib import Path state_root = Path( os.environ.get( "XDG_STATE_HOME", Path.home() / ".local" / "state" ) ) / "ham-radio-dx" state_root.mkdir(mode=0o700, parents=True, exist_ok=True) os.chmod(state_root, 0o700) state_file = state_root / "state.json" def save_state(state): fd, temporary_path = tempfile.mkstemp( prefix=".state-", dir=state_root ) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w") as output: json.dump(state, output, indent=2) output.flush() os.fsync(output.fileno()) os.replace(temporary_path, state_file) except Exception: try: os.unlink(temporary_path) except FileNotFoundError: pass raise- Replace the shared
