T09 · Insecure Skill Coding Practices
- Location
src/daily_strava_roast/cli.py:67- Finding
Strava tokens and fitness-history state are written without restrictive file permissions
- Content
View full analysis
None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, indent=2) + "\n") ``` ```python def save_state(path: Path, state: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(state, indent=2) + "\n") ``` The token data written by `save_tokens` includes Strava access and refresh tokens. The state written by `save_state` includes activity names, dates, sports, distances, durations, elevation, kudos, and generated roast text. ### Technical Analysis `Path.write_text()` creates files according to the process umask and does not enforce owner-only permissions. In environments with a typical `022` umask, a newly created token or state file can be created with mode `0644`, allowing other local users to read it. The parent directories are also created without an explicit restrictive mode. This is especially serious for the refresh token because it can be exchanged at Strava's OAuth endpoint for new access tokens. The state file does not contain OAuth credentials, but it contains persistent personal fitness and behavioral data. Writing the files non-atomically also creates a risk of partial credential files if the process is interrupted. The implementation does not reject symbolic links, so a writable or insufficiently protected parent directory could additionally permit link-based file redirection. The network use itself is appropriate for the declared functionality: credentials are sent only to the fixed HTTPS Strava OAuth endpoint, and the bearer token is sent only to the fixed HTTPS Strava API endpoint. The local storage controls, however, do ...[truncated 1449 chars]- Remediation
View remediation
None: path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(path.parent, 0o700) fd, temporary_name = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.") try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(data, handle, indent=2) handle.write("\n") handle.flush() os.fsync(handle.fileno()) os.replace(temporary_name, path) os.chmod(path, 0o600) except Exception: try: os.unlink(temporary_name) except FileNotFoundError: pass raise ``` ]]>
