Bound native hook permission fingerprints#71758
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟡 Unbounded JSON hashing of permission-request toolInput can cause CPU DoS
Description
The recent change attempts to avoid sorting a very large keyset by only sorting the first
If an attacker can influence the native hook payload (e.g., via prompt/tool-input injection or untrusted hook source), they can submit an extremely large object (hundreds of thousands of keys / deep nested arrays) and force expensive hashing on the relay process, degrading availability. Vulnerable code: if (truncated) {
const sortedKeySet = new Set(keys);
hash.update("#object-tail:");
for (const key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key) || sortedKeySet.has(key)) {
continue;
}
hash.update(JSON.stringify(key));
hash.update(":");
updateJsonHash(hash, value[key]);
hash.update(",");
}
}RecommendationAdd hard caps to fingerprinting work (total keys, total array items, and recursion depth), and when limits are exceeded, include a deterministic truncation marker rather than traversing the remainder. Example approach: const MAX_TOTAL_NODES = 10_000;
const MAX_DEPTH = 50;
function updateJsonHashBounded(hash: Hash, value: JsonValue, state = { nodes: 0, depth: 0 }): void {
if (state.nodes++ > MAX_TOTAL_NODES || state.depth > MAX_DEPTH) {
hash.update("#truncated");
return;
}
// ...same logic as updateJsonHash, but:
// - only hash first N keys/items
// - avoid tail full traversal; append '#object-tail-truncated' marker
// - increment/decrement state.depth for recursion
}Also consider enforcing a maximum size/shape for Analyzed PR: #71758 at commit Last updated on: 2026-04-25T21:06:08Z |
Greptile SummaryThis PR bounds the
Confidence Score: 3/5P1 behavioral regression in budget semantics could exhaust the rate-limit window under concurrent duplicate permission requests; safe fallback exists but dedup guarantee is broken. One P1 finding (budget consumed before dedup check changes rate-limit semantics and drops pending-approval sharing for over-budget requests) pulls the score to 3. A P2 finding (order-dependent content fingerprint) adds further concern. The fallback to the provider approval path prevents a hard denial, but the correctness regression in the deduplication path is real. src/agents/harness/native-hook-relay.ts — specifically the budget gate placement relative to the pending-approval map lookup (lines 438–454) and the updateJsonHash key iteration order (lines 559–569). Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/harness/native-hook-relay.ts
Line: 438-443
Comment:
**Budget consumed before dedup check exhausts window with concurrent identical requests**
Moving `consumeNativeHookRelayPermissionBudget` before the pending-approval lookup changes the budget semantics from "unique approval prompts launched" to "total relay permission calls processed." Under the old code, N concurrent requests for the same approval key each shared the one pending promise without consuming N budget slots. Now every concurrent duplicate consumes a slot, so 12 identical in-flight permission requests exhaust the full `MAX_PERMISSION_APPROVALS_PER_WINDOW` (12) budget — causing the 13th call for a genuinely new permission to return noop/defer in the same minute. Additionally, any request that arrives over-budget can no longer attach to an already-pending promise: it returns noop immediately rather than inheriting the in-flight decision.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/agents/harness/native-hook-relay.ts
Line: 559-569
Comment:
**Content fingerprint is now key-insertion-order dependent**
Replacing `Object.keys(value).toSorted()` with `for (const key in value)` means two objects with identical key/value pairs but different insertion orders (e.g. `{a:1, b:2}` vs `{b:2, a:1}`) will produce different SHA-256 fingerprints. Since the content fingerprint is part of `approvalKey`, those two requests won't deduplicate and the user could see duplicate approval prompts for the same logical operation. In practice JavaScript engines preserve insertion order and JSON deserialization is deterministic, but any code path that constructs `toolInput` differently (e.g. via object spread in a different order) will silently break dedup.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix: bound native hook permission finger..." | Re-trigger Greptile |
fabianwilliams
left a comment
There was a problem hiding this comment.
Solid hardening — dedup of pending PermissionRequest approvals before they consume budget, plus key-order-stable canonical fingerprints, plus a sensitivity test for tail mutations on broad inputs. The Object.keys-spy test catches future enumeration regressions. LGTM.
* fix: bound native hook permission fingerprints * fix: address native hook fingerprint review * test: isolate native jiti runtime assertions
* fix: bound native hook permission fingerprints * fix: address native hook fingerprint review * test: isolate native jiti runtime assertions
* fix: bound native hook permission fingerprints * fix: address native hook fingerprint review * test: isolate native jiti runtime assertions
* fix: bound native hook permission fingerprints * fix: address native hook fingerprint review * test: isolate native jiti runtime assertions
* fix: bound native hook permission fingerprints * fix: address native hook fingerprint review * test: isolate native jiti runtime assertions
* fix: bound native hook permission fingerprints * fix: address native hook fingerprint review * test: isolate native jiti runtime assertions
* fix: bound native hook permission fingerprints * fix: address native hook fingerprint review * test: isolate native jiti runtime assertions
PR #71707 added the Codex MCP native hook bridge and tightened the relay around native tool and permission events. A late security pass pointed out one remaining sharp edge in the new PermissionRequest dedupe path: even though relay payloads are already JSON-budgeted, the approval key still sorted attacker-controlled
tool_inputkeys when building fallback and content fingerprints.This follow-up keeps the same approval behavior but removes that sorting from the hot path. The fallback key now collects only a bounded set of own keys before sorting that small subset, and the content fingerprint hashes object keys in traversal order instead of materializing and sorting the full key list. PermissionRequest budget consumption also happens before the approval key is built, so repeated broad payloads defer to the provider approval path before doing the extra fingerprint work.
The regression test now proves broad PermissionRequest inputs can be fingerprinted without calling
Object.keys, which is the specific operation the review flagged.