Skip to content

fix(cli): harden context graph ids and keystores#296

Open
branarakic wants to merge 5 commits intomainfrom
codex/small-ci-fixes
Open

fix(cli): harden context graph ids and keystores#296
branarakic wants to merge 5 commits intomainfrom
codex/small-ci-fixes

Conversation

@branarakic
Copy link
Copy Markdown
Contributor

Summary

  • Reject context graph IDs containing literal or URL-encoded traversal path segments before route handlers create local graphs.
  • Validate stored scrypt KDF parameters before keystore decryption, including N/r/p floors, salt length, and dklen.
  • Add focused unit coverage for the ID validator and update keystore tests to the documented minimum test cost.

Test plan

  • pnpm --filter @origintrail-official/dkg build
  • pnpm exec vitest run packages/cli/test/http-utils.test.ts packages/cli/test/keystore.test.ts packages/cli/test/daemon-keystore-extra.test.ts

Note: 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

Comment thread packages/cli/src/daemon/http-utils.ts Outdated
if (!/^[\w:/.@\-]+$/.test(id)) return false;
if (id.includes('\\')) return false;
try {
const decoded = decodeURIComponent(id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

@branarakic
Copy link
Copy Markdown
Contributor Author

Addressed the traversal-bypass review in 4547d89:

  • Moved literal/URL-encoded ./.. segment rejection into the shared @origintrail-official/dkg-core validateContextGraphId() validator.
  • Made the CLI isValidContextGraphId() delegate to that shared validator.
  • Added validation to decoded context-graph path-param routes before they call agent methods.
  • Added core validator coverage and a daemon HTTP regression for an encoded traversal path-param route.

Validation:

  • pnpm --filter @origintrail-official/dkg-core build
  • pnpm --filter @origintrail-official/dkg build
  • pnpm exec vitest run packages/core/test/constants.test.ts packages/cli/test/http-utils.test.ts packages/cli/test/keystore.test.ts packages/cli/test/daemon-keystore-extra.test.ts

assertSafeKdfParams(kdfparams);
const salt = Buffer.from(kdfparams.salt, 'hex');
const key = deriveKey(passphrase, salt);
const key = deriveKey(passphrase, salt, kdfparams);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

@branarakic
Copy link
Copy Markdown
Contributor Author

Addressed the KDF upper-bound review in b4f5ff6:

  • decryptKeystore() now rejects scrypt N/r/p values above the supported writer envelope before calling synchronous scryptSync.
  • Added regression coverage for oversized N/r/p values.

Validation:

  • pnpm --filter @origintrail-official/dkg build
  • pnpm exec vitest run packages/cli/test/keystore.test.ts packages/cli/test/daemon-keystore-extra.test.ts

const addParticipantMatch = path.match(/^\/api\/context-graph\/([^/]+)\/add-participant$/);
if (req.method === "POST" && addParticipantMatch) {
const contextGraphId = decodeURIComponent(addParticipantMatch[1]);
if (!validateRequiredContextGraphId(contextGraphId, res)) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Comment thread packages/cli/src/keystore.ts Outdated
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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

@branarakic
Copy link
Copy Markdown
Contributor Author

Addressed the latest review in ee87bb3:

  • Path-param context-graph routes now use safeDecodeURIComponent() before validation, so malformed percent-encoding returns 400 instead of bubbling a URIError.
  • Keystore KDF validation now uses a memory-cost bound for N/r instead of pinning r to the current writer value, and keeps a bounded p envelope before calling synchronous scryptSync.
  • Added regression coverage for malformed path params and oversized KDF resource costs.

Validation:

  • pnpm --filter @origintrail-official/dkg build
  • pnpm exec vitest run packages/cli/test/keystore.test.ts packages/cli/test/daemon-keystore-extra.test.ts packages/core/test/constants.test.ts packages/cli/test/http-utils.test.ts

return Number.isInteger(value) && value > 0 && Number.isInteger(Math.log2(value));
}

function assertSafeKdfParams(kdfparams: EncryptedKeystore['crypto']['kdfparams']): void {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment thread packages/core/src/constants.ts Outdated
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@branarakic
Copy link
Copy Markdown
Contributor Author

Addressed the final self-consistency/percent-decoding feedback in c887970:

  • _setScryptN() now rejects unsupported encryption costs, so the writer cannot emit keystores the reader refuses.
  • Core validateContextGraphId() now only handles already-decoded IDs and literal traversal segments; malformed percent-encoding stays at the HTTP safeDecodeURIComponent() boundary.
  • Adjusted the KDF and core validator tests accordingly.

Validation:

  • pnpm --filter @origintrail-official/dkg-core build
  • pnpm --filter @origintrail-official/dkg build
  • pnpm exec vitest run packages/core/test/constants.test.ts packages/cli/test/http-utils.test.ts packages/cli/test/keystore.test.ts packages/cli/test/daemon-keystore-extra.test.ts

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 === '..')) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant