T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/invoice.py:244
- Finding
- API Credentials Stored in Plaintext Without Enforced Access Restrictions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/invoice.py:244-249`; `scripts/setup.py:46-49` **Vulnerability Type**: Plaintext credential storage with filesystem permissions inherited from the process environment **Risk Level**: Medium ### Vulnerable Code `scripts/invoice.py:244-249`: ```python def save_credentials(key, secret): config_path = get_config_path() config_path.parent.mkdir(parents=True, exist_ok=True) with open(config_path, "w", encoding="utf-8") as f: json.dump({"key": key, "secret": secret}, f, ensure_ascii=False, indent=2) print(f" [CONFIG] 凭据已保存至:{config_path}") ``` `scripts/setup.py:46-49`: ```python config_path.parent.mkdir(parents=True, exist_ok=True) with open(config_path, "w", encoding="utf-8") as f: json.dump({"key": key, "secret": secret}, f, ensure_ascii=False, indent=2) print(f"\n[OK] 凭据已保存至:{config_path}") ``` ### Technical Analysis The Skill stores the NetOCR API key and secret directly in `config.json` as unencrypted JSON. Neither credential-writing path explicitly applies restrictive permissions such as mode `0600`. The effective permissions therefore depend on the host process's umask and existing file permissions. On a shared or incorrectly configured host, the resulting file may be readable by other local accounts, services, backup agents, development tools, or processes running under a different security context. If an existing `config.json` has permissive permissions, opening it with `"w"` truncates and rewrites the file but does not correct those permissions. The credential storage supports the declared OCR functionality, but unrestricted plaintext persistence is not the minimum-risk mechanism required to provide that functionality. Environment variables, an operating-system credential manager, or a permission-restricted configuration file would reduce exposure. No hardcoded live credentials were found in the audited `config.json`; the issue concerns credentials entered d ...[truncated 1339 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Prefer an operating-system credential manager or secret-management service instead of a project-local plaintext file. 2. If file-based storage is unavoidable, create the credential file atomically with owner-only permissions: ```python import os import json import tempfile from pathlib import Path def save_credentials(key, secret): config_path = get_config_path() config_path.parent.mkdir(parents=True, exist_ok=True) fd, temp_name = tempfile.mkstemp( dir=str(config_path.parent), prefix=".config.", text=True, ) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump( {"key": key, "secret": secret}, f, ensure_ascii=False, indent=2, ) f.flush() os.fsync(f.fileno()) os.replace(temp_name, config_path) os.chmod(config_path, 0o600) except Exception: try: os.unlink(temp_name) except OSError: pass raise ``` 3. Before reading an existing credential file, inspect its ownership and mode and reject or warn about group/world-readable permissions. 4. Add `config.json` to version-control ignore rules and exclude it from logs, support bundles, backups, and synchronization where practical. 5. Prefer hidden input for the secret, such as `getpass.getpass()`, to prevent terminal echo. 6. Document credential rotation and revoke credentials immediately if the file may have been exposed. ]]>
