T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/tempmail_otp.py:40
- Finding
- Sensitive state files are created with insufficient permission controls<![CDATA[ ## Vulnerability Details **File Locations**: - `scripts/tempmail_otp.py:40-49` - `scripts/tempmail_otp.py:316-318` - `scripts/tempmail_otp.py:348-350` **Vulnerability Type**: Insecure storage of sensitive credentials, OTPs, and verification links **Risk Level**: Medium ### Vulnerable Code ```python def ensure_state_dir(): """Create state directory if it doesn't exist.""" os.makedirs(STATE_DIR, exist_ok=True) def save_state(data: dict): """Save account state to file.""" ensure_state_dir() with open(STATE_FILE, "w") as f: json.dump(data, f, indent=2) os.chmod(STATE_FILE, 0o600) # Restrictive permissions ``` ```python # Save OTP to file ensure_state_dir() with open(LAST_OTP_FILE, "w") as f: f.write(otp) ``` ```python # Save first interesting link if interesting_urls: ensure_state_dir() with open(LAST_LINK_FILE, "w") as f: f.write(interesting_urls[0]) ``` ### Technical Analysis The state directory is created without explicitly enforcing mode `0700`. Its effective permissions therefore depend on the process umask and any pre-existing directory permissions. The account state file contains the temporary mailbox address, plaintext password, and bearer JWT. It is opened and written before `os.chmod(STATE_FILE, 0o600)` is called. Consequently, a newly created file initially receives permissions derived from the ambient umask. This creates a time-of-check/time-of-protection window during which another local user may be able to read it. If writing or serialization fails before `chmod` executes, the file can remain with the initial permissions. The `last_otp` and `last_link` files receive no explicit permission hardening at all. Under a common `022` umask, newly created files ordinarily have mode `0644`, making them readable by other local users. This contradicts the documentation's assertion that all state files use mode `0600`. A verification URL can itself function as a bearer credential. Reading ei ...[truncated 2201 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Create and enforce the state directory with owner-only permissions: ```python def ensure_state_dir(): os.makedirs(STATE_DIR, mode=0o700, exist_ok=True) os.chmod(STATE_DIR, 0o700) ``` 2. Use one secure helper for every sensitive state file. Create temporary files atomically with mode `0600`, write and flush the data, and then replace the destination: ```python import tempfile def secure_write(path: str, content: str): ensure_state_dir() fd, temp_path = tempfile.mkstemp(dir=STATE_DIR) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w") as f: f.write(content) f.flush() os.fsync(f.fileno()) os.replace(temp_path, path) os.chmod(path, 0o600) except Exception: try: os.close(fd) except OSError: pass try: os.unlink(temp_path) except OSError: pass raise ``` 3. Apply the secure writer consistently to: - `account.json` - `last_otp` - `last_link` 4. Validate that the state directory is owned by the current user and is not a symbolic link before storing credentials. 5. Where supported, use no-follow filesystem semantics such as `O_NOFOLLOW` when opening sensitive paths. 6. Avoid printing the plaintext mailbox password by default. Require an explicit option when credential output is necessary, and ensure JSON output is treated as sensitive. 7. Add automated tests that run under permissive umasks and verify that the directory remains `0700` and every sensitive file remains `0600`. ]]>
