fix(cli): harden context graph ids and keystores#296
fix(cli): harden context graph ids and keystores#296branarakic wants to merge 5 commits intomainfrom
Conversation
Made-with: Cursor
| if (!/^[\w:/.@\-]+$/.test(id)) return false; | ||
| if (id.includes('\\')) return false; | ||
| try { | ||
| const decoded = decodeURIComponent(id); |
There was a problem hiding this comment.
🔴 Bug: This traversal guard only protects the handful of routes that call isValidContextGraphId(). The /:id/* context-graph endpoints still decodeURIComponent() the path segment and pass it straight to agent methods without validation, and most other request-body routes still go through @origintrail-official/dkg-core's validateContextGraphId(), which still accepts ../.... That leaves an easy bypass for the fix. Move the ./.. segment check into the shared validator (or validate every decoded contextGraphId) and add a regression test against one of the path-param routes.
Made-with: Cursor
|
Addressed the traversal-bypass review in 4547d89:
Validation:
|
| assertSafeKdfParams(kdfparams); | ||
| const salt = Buffer.from(kdfparams.salt, 'hex'); | ||
| const key = deriveKey(passphrase, salt); | ||
| const key = deriveKey(passphrase, salt, kdfparams); |
There was a problem hiding this comment.
🔴 Bug: decryptKeystore now feeds attacker-controlled kdfparams directly into scryptSync, but the new validation only checks minimums. A tampered keystore can still set very large n/r/p values and either block the process in a synchronous KDF or bubble up a raw crypto RangeError. Please cap these fields to the supported envelope here (or require the exact params this writer emits) before deriving the key.
Made-with: Cursor
|
Addressed the KDF upper-bound review in b4f5ff6:
Validation:
|
| const addParticipantMatch = path.match(/^\/api\/context-graph\/([^/]+)\/add-participant$/); | ||
| if (req.method === "POST" && addParticipantMatch) { | ||
| const contextGraphId = decodeURIComponent(addParticipantMatch[1]); | ||
| if (!validateRequiredContextGraphId(contextGraphId, res)) return; |
There was a problem hiding this comment.
🔴 Bug: This guard never sees malformed percent-encoding, because decodeURIComponent(addParticipantMatch[1]) can throw before validation runs. The daemon's top-level error handler maps that URIError to 500, so these new path-param routes are still crashable with a bad % sequence instead of returning 400. Use the existing safeDecodeURIComponent(...) helper (or wrap the decode in try/catch) before validateRequiredContextGraphId, and apply the same fix to the other {id} handlers added in this PR.
| if (!Number.isInteger(kdfparams.r) || kdfparams.r < MIN_SCRYPT_R) { | ||
| throw new Error('KDF parameters below minimum: scrypt r too low'); | ||
| } | ||
| if (kdfparams.r > MAX_SCRYPT_R) { |
There was a problem hiding this comment.
🔴 Bug: Rejecting any r > 8 / p > 1 makes decryptKeystore incompatible with keystores that are stronger than the one we currently emit. A valid scrypt keystore from another tool, or a future version that raises the work factor, will now hard-fail before decryption even though it is safer than today's defaults. Keep the minimum floor checks, but use a resource bound (maxmem / estimated cost) instead of pinning the reader to exactly our current r and p values.
Made-with: Cursor
|
Addressed the latest review in ee87bb3:
Validation:
|
| return Number.isInteger(value) && value > 0 && Number.isInteger(Math.log2(value)); | ||
| } | ||
|
|
||
| function assertSafeKdfParams(kdfparams: EncryptedKeystore['crypto']['kdfparams']): void { |
There was a problem hiding this comment.
🟡 Issue: decryption now rejects n < 2**15, but encryptKeystore() still uses the mutable SCRYPT_N without the same guard. Because _setScryptN() is still exported, tests/scripts can generate keystores that this module will no longer decrypt. Enforce the floor on the write path as well (or reject low values in _setScryptN) so the API stays self-consistent.
| if (!id || id.length === 0) return { valid: false, reason: 'Context graph ID cannot be empty' }; | ||
| if (id.length > 256) return { valid: false, reason: 'Context graph ID exceeds 256 characters' }; | ||
| if (!/^[\w:/.@\-]+$/.test(id)) return { valid: false, reason: 'Context graph ID contains disallowed characters (allowed: alphanumeric, _, :, /, ., @, -)' }; | ||
| let decoded: string; |
There was a problem hiding this comment.
🟡 Issue: this extra decodeURIComponent() is effectively dead for direct callers of validateContextGraphId() because the whitelist above still rejects % first. That means the new malformed-encoding/traversal branch only runs after a caller has already decoded once, and the added core test passes for the wrong reason. Either allow % through the whitelist and let this block validate it, or drop the second decode and keep percent-decoding checks strictly at the HTTP boundary.
Made-with: Cursor
|
Addressed the final self-consistency/percent-decoding feedback in c887970:
Validation:
|
| if (!id || id.length === 0) return { valid: false, reason: 'Context graph ID cannot be empty' }; | ||
| if (id.length > 256) return { valid: false, reason: 'Context graph ID exceeds 256 characters' }; | ||
| if (!/^[\w:/.@\-]+$/.test(id)) return { valid: false, reason: 'Context graph ID contains disallowed characters (allowed: alphanumeric, _, :, /, ., @, -)' }; | ||
| if (id.split('/').some((segment) => segment === '.' || segment === '..')) { |
There was a problem hiding this comment.
🔴 Bug: This only rejects literal . / .. segments. isValidContextGraphId('legit-cg/%2e%2e/other-cg') still returns true, so the new packages/cli/test/http-utils.test.ts case fails and any body/query caller that doesn't go through safeDecodeURIComponent can still bypass the traversal guard by percent-encoding the segment. Decode once before splitting, or explicitly reject %2e/%2e%2e case-insensitively in the shared validator.
Summary
Test plan
pnpm --filter @origintrail-official/dkg buildpnpm exec vitest run packages/cli/test/http-utils.test.ts packages/cli/test/keystore.test.ts packages/cli/test/daemon-keystore-extra.test.tsNote: I attempted the HTTP behavior test filter for CLI-16, but that suite requires the shared Hardhat context file and failed locally before test execution because the context file was absent.
Made with Cursor