T09 · Insecure Skill Coding Practices
- Location
scripts/vault.js:6- Finding
Keystore Path Traversal Through Attacker-Controlled Identity Names
- Content
View full analysis
Vulnerability Details
File Location:
scripts/vault.js:6-33
Vulnerability Type: Path traversal and unauthorized filesystem access
Risk Level: HighVulnerable Code
js const VAULT_PATH = path.join(process.cwd(), 'data/keystore'); module.exports = { getOrGenerateIdentity: async (localAgentId) => { await sodium.ready; if (!fs.existsSync(VAULT_PATH)) fs.mkdirSync(VAULT_PATH, { recursive: true }); const keyPath = path.join(VAULT_PATH, `${localAgentId}.keys.json`); if (fs.existsSync(keyPath)) { const keys = JSON.parse(fs.readFileSync(keyPath)); return { publicKey: Buffer.from(keys.publicKey, 'hex'), privateKey: Buffer.from(keys.privateKey, 'hex') }; } const keypair = sodium.crypto_sign_keypair(); fs.writeFileSync(keyPath, JSON.stringify({ publicKey: sodium.to_hex(keypair.publicKey), privateKey: sodium.to_hex(keypair.privateKey) }), { mode: 0o600 }); return { publicKey: keypair.publicKey, privateKey: keypair.privateKey }; }, signPayload: async (localAgentId, payloadString) => { await sodium.ready; const keyPath = path.join(VAULT_PATH, `${localAgentId}.keys.json`); const keys = JSON.parse(fs.readFileSync(keyPath)); const signature = sodium.crypto_sign_detached( payloadString, Buffer.from(keys.privateKey, 'hex') ); return sodium.to_hex(signature); },The attacker-controlled values originate from
index.js:26-27andindex.js:65-67:js async function registerIdentity(params) { const localAgentId = params.alias || `agent-${uuidv4()}`; const keys = await vault.getOrGenerateIdentity(localAgentId); } async function signMessage(params) { const { localId, payload } = params; const contentStr = typeof payload === 'string' ? payload : JSON.stringify(payload); const signatureHex = await vault.signPayload(local ...[truncated 1972 chars]- Remediation
View remediation
Remediation Suggestions
-
Restrict identity names to a conservative allowlist, such as
/^[A-Za-z0-9_-]{1,64}$/. -
Reject path separators,
.and..components, control characters, and platform-specific separator variants. -
Resolve the final path and verify containment before every read or write:
js const base = path.resolve(VAULT_PATH); const candidate = path.resolve(base, `${localAgentId}.keys.json`); if (!candidate.startsWith(base + path.sep)) { throw new Error('Invalid identity identifier'); } -
Use opaque, randomly generated identifiers as file names rather than user-provided aliases.
-
Store the human-readable alias as validated metadata instead of using it as a path component.
-
Protect against symlink-based redirection by checking path components and using safe file-open flags.
-
Consider exclusive creation (
wx) when creating new identities to prevent unintended replacement. -
Apply the same centralized path-validation routine to registration and signing.
-
