T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/setup.py:29
- Finding
- API Keys Are Echoed During Entry and Stored Without Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:29-31`, `scripts/setup.py:36-48`, and `scripts/setup.py:107-116` **Vulnerability Type**: Plaintext credential exposure and insecure local secret storage **Risk Level**: High ### Vulnerable Code ```python def save_config(config: dict): """Save config to file.""" config_path = get_config_path() with open(config_path, "w") as f: json.dump(config, f, indent=2) print(f"\n✅ Config saved to: {config_path}") ``` ```python def prompt_api_key(name: str, description: str, existing: str = "") -> str: """Prompt user for an API key.""" print(f"\n{name}") print("-" * len(name)) print(description) if existing: print(f"Current: {'*' * 8}{existing[-4:] if len(existing) > 4 else ''}") print("Press Enter to keep existing, or type new key:") else: print("Enter API key (leave empty to skip):") value = input("> ").strip() return value if value else existing ``` ```python for svc in services: existing = config.get(svc["key"], "") value = prompt_api_key( svc["name"], svc["desc"], existing ) if value: config[svc["key"]] = value # Save configuration ``` ### Technical Analysis The setup wizard reads API keys through Python's regular `input()` function. Terminal echo remains enabled, so each key is displayed while the user types it. This exposes credentials to screen recording, terminal capture, nearby observers, and some terminal logging mechanisms. The resulting keys are written in plaintext to the Skill-local `config.json` using the process's default file creation mode. The code does not: - Explicitly create the file with mode `0600`. - verify or correct the permissions of an existing file; - use an operating-system credential store; - perform an atomic, securely permissioned replacement; - warn users that the file contains reusable plaintext credentials. The actual expos ...[truncated 1402 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Replace `input()` with `getpass.getpass()` so secrets are not echoed: ```python from getpass import getpass value = getpass("> ").strip() ``` 2. Prefer environment variables or an operating-system keyring instead of project-local plaintext storage. 3. If file storage remains supported, create the file atomically with owner-only permissions: ```python import os import tempfile config_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) fd, temporary_path = tempfile.mkstemp(dir=config_path.parent) try: os.fchmod(fd, 0o600) with os.fdopen(fd, "w") as stream: json.dump(config, stream, indent=2) stream.flush() os.fsync(stream.fileno()) os.replace(temporary_path, config_path) os.chmod(config_path, 0o600) except Exception: try: os.unlink(temporary_path) except OSError: pass raise ``` 4. Check existing file permissions before reading or updating the configuration and reject group- or world-readable files. 5. Add `config.json` and `.cache/` to source-control ignore rules. 6. Clearly document that saved keys are reusable secrets and provide instructions for rotation after suspected exposure. ]]>
