T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/init_config.py:54
- Finding
- API credentials are visibly collected and stored without enforced restrictive permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/init_config.py:54-82` **Related Documentation**: `scripts/init_config.py:89-92`, `SKILL.md:21-23` **Vulnerability Type**: Plaintext credential exposure and insecure file permissions **Risk Level**: Medium ### Vulnerable Code ```python token = input("Token: ").strip() if not token: print("❌ Token is required") return None app_id = input("App ID (optional, press Enter to skip): ").strip() api_key = input("API Key (optional, press Enter to skip): ").strip() # Create config config = { "auth": { "token": token, }, "baseUrl": "https://openapi.geelark.com", "rateLimit": { "perMinute": 200, "perHour": 24000 } } if app_id: config["auth"]["appId"] = app_id if api_key: config["auth"]["apiKey"] = api_key # Save config with open(config_path, 'w', encoding='utf-8') as f: json.dump(config, f, indent=2, ensure_ascii=False) ``` The script subsequently claims that the credential file is protected: ```python print(" - config.json contains your sensitive credentials") print(" - Do NOT commit config.json to version control") print(" - config.json is already in .gitignore") ``` ### Technical Analysis The initializer uses `input()` for the bearer token and API key. Unlike a secret-aware prompt such as `getpass.getpass()`, `input()` displays the entered value on the terminal. This can expose credentials through shoulder surfing, screen recording, terminal-sharing sessions, or captured interactive output. The credentials are then written as plaintext using the process's default file-creation mode. The code neither creates the file with mode `0600` nor applies `os.chmod(config_path, 0o600)` after writing it. Consequently, effective access depends on the user's umask and environment. Under a permissive configuration, other local users or processes may be able to read the file. The audited project structure did not contain the `.gitignore` fi ...[truncated 1913 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Use non-echoing prompts for all secrets: ```python from getpass import getpass token = getpass("Token: ").strip() api_key = getpass("API Key (optional, press Enter to skip): ").strip() ``` 2. Create the configuration file atomically with owner-only permissions: ```python import os import json import tempfile fd, temporary_path = tempfile.mkstemp( dir=assets_dir, prefix=".config-", text=True ) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False) f.flush() os.fsync(f.fileno()) os.replace(temporary_path, config_path) os.chmod(config_path, 0o600) except Exception: try: os.unlink(temporary_path) except OSError: pass raise ``` 3. Add and distribute an actual `.gitignore` containing at least: ```gitignore assets/config.json logs/ ``` 4. Remove or correct the claim that `.gitignore` protection already exists unless the file is included and verified. 5. Prefer an operating-system credential store, injected environment secret, or dedicated secret manager instead of long-term plaintext token storage. 6. On startup, verify that the credential file is owned by the current user and is not group- or world-readable. Refuse to continue or display a prominent warning when permissions are unsafe. 7. Document token rotation and immediate revocation procedures for suspected exposure. ]]>
