T09 · Insecure Skill Coding Practices
- Location
scripts/register.mjs:65- Finding
Insecure API Credential Storage and Partial Secret Disclosure
- Content
View full analysis
Vulnerability Details
File Locations:
scripts/register.mjs:30-36scripts/register.mjs:65-74scripts/check.mjs:22-24
Vulnerability Type: Insecure storage and disclosure of API credentials
Risk Level: MediumTechnical Analysis
The registration script stores an API key in
~/.openclaw/credentials/quack.jsonwithout explicitly applying restrictive filesystem permissions:js // Save credentials if (data.apiKey || data.agentId) { mkdirSync(CREDS_DIR, { recursive: true }); const creds = { agentId: data.agentId || args.agentId, apiKey: data.apiKey || null, badge: data.badge || null, quckGrant: data.quckGrant || 0, registeredAt: new Date().toISOString(), }; writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2)); console.log(`Credentials saved to ${CREDS_FILE}`); }Because neither
mkdirSyncnorwriteFileSyncspecifies a mode, effective permissions depend on the process umask and existing path permissions. Common defaults may create the directory as0755and the file as0644, potentially allowing other local users to read the complete API key on a shared system.The registration script also prints the first 12 characters of an existing API key:
js if (existsSync(CREDS_FILE)) { const existing = JSON.parse(readFileSync(CREDS_FILE, 'utf8')); console.log(`Already registered as ${existing.agentId}`); console.log(`API Key: ${existing.apiKey?.substring(0, 12)}...`); console.log('To re-register, delete ~/.openclaw/credentials/quack.json first.'); return; }The status-checking script repeats this disclosure:
js if (creds.apiKey) { console.log(` API Key: ${creds.apiKey.substring(0, 12)}...`); }Terminal output may be retained in shell logs, CI logs, agent transcripts, or monitoring systems. Revealing a key prefix is unnecessary for checking registration status and can facilitate credential correlation, identification, or attacks against weak or predictable token ...[truncated 1763 chars]
- Remediation
View remediation
Remediation Suggestions
- Create the credentials directory with owner-only permissions:
js mkdirSync(CREDS_DIR, { recursive: true, mode: 0o700, });- Write the credential file with mode
0600:
js writeFileSync( CREDS_FILE, JSON.stringify(creds, null, 2), { encoding: 'utf8', mode: 0o600, }, );- Explicitly tighten permissions on pre-existing paths, because creation modes do not correct permissions on files or directories that already exist:
js import { chmodSync } from 'fs'; chmodSync(CREDS_DIR, 0o700); chmodSync(CREDS_FILE, 0o600);-
Avoid following attacker-controlled symbolic links. Validate the destination and use exclusive or atomic file creation where practical, such as writing securely to a same-directory temporary file and renaming it into place.
-
Remove all API-key prefix output. Report only whether a credential exists:
js console.log(`API Key: ${existing.apiKey ? 'configured' : 'not configured'}`);js if (creds.apiKey) { console.log(' API Key: configured'); }-
Document that the file contains a sensitive bearer credential and should not be copied into source control, logs, diagnostics, backups without access controls, or agent transcripts.
-
Consider using an operating-system credential store instead of a plaintext JSON file when the supported runtime environment provides one.
