T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/add-model-guide.py:199
- Finding
- API Keys Are Echoed and Stored Without Enforced Owner-Only Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/add-model-guide.py`, lines 119 and 199-218 **Vulnerability Type**: Plaintext credential handling and insecure file permissions **Risk Level**: Medium ### Vulnerable Code ```python api_key = input(f"{Colors.YELLOW}请输入 API Key(回车跳过):{Colors.NC} ").strip() ``` ```python def save_json(path, data): with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) ``` ```python # Update openclaw.json if API key provided if api_key: openclaw = load_json(OPENCLAW_CONFIG) if OPENCLAW_CONFIG.exists() else {} if 'models' not in openclaw: openclaw['models'] = {'mode': 'merge', 'providers': {}} if 'providers' not in openclaw['models']: openclaw['models']['providers'] = {} provider_id = provider['id'] if provider_id not in openclaw['models']['providers']: openclaw['models']['providers'][provider_id] = { 'baseUrl': f"https://api.{provider_id}.com/v1" if provider_id != 'custom' else "", 'apiKey': api_key, 'api': 'openai-completions', 'models': [] } else: openclaw['models']['providers'][provider_id]['apiKey'] = api_key save_json(OPENCLAW_CONFIG, openclaw) ``` ### Technical Analysis The script obtains an API key through the standard `input()` function. Terminal input is therefore visible while the user types it and may be captured through shoulder surfing, terminal recording, or other session-monitoring mechanisms. The key is then stored in plaintext in `~/.openclaw/openclaw.json`. The generic `save_json()` function opens the destination using normal process defaults and does not explicitly create the file with mode `0600` or verify its permissions after writing. When the file is newly created, its resulting permissions depend on the user's process umask. A common umask can produce a file readable by other local users. The documentation warns that keys a ...[truncated 1277 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Read credentials with `getpass.getpass()` so they are not echoed: ```python from getpass import getpass api_key = getpass("Enter API Key: ").strip() ``` 2. Create the configuration with owner-only permissions: ```python import os fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) ``` 3. Explicitly enforce `0600` after writing, including when updating an existing file: ```python os.chmod(path, 0o600) ``` 4. Write to an owner-only temporary file and atomically replace the destination to avoid partially written configuration data. 5. Prefer an operating-system credential store, dedicated secret manager, or environment-variable reference instead of embedding the API key directly in JSON. 6. Detect unsafe existing permissions and either correct them automatically or stop with a clear warning before writing the secret. ]]>
