refactor: canonicalize chat.send/cron route derivation#35451
Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Cron outbound delivery can leak outputs by inheriting mutable main-session last route (fail-open recipient selection)
Description
In multi-user/shared deployments (where multiple users can update the shared main session’s
This is especially risky for cron jobs that omit an explicit Vulnerable code: // defaults to main session entry and does not enable fail-closed last-route blocking
const main = threadEntry ?? store[mainSessionKey];
// Intentionally do not set `failClosedMainSessionLastRoute` here...
const preliminary = resolveSessionDeliveryTarget({
entry: main,
requestedChannel,
explicitTo,
allowMismatchedLastTo,
sessionKey: sessionRouteKey,
mainKey: sessionMainKey,
});RecommendationAvoid inheriting Options (prefer combining 1 + 2):
// compute intent before resolving target
const deliveryBestEffort = resolveCronDeliveryBestEffort(job);
const deliveryIntent: DeliveryIntent = !deliveryRequested || deliveryPlan.mode === "none"
? "internal_only"
: deliveryBestEffort ? "external_preferred" : "external_required";
const failClosed = deliveryIntent !== "internal_only";
const preliminary = resolveSessionDeliveryTarget({
entry: main,
requestedChannel,
explicitTo,
allowMismatchedLastTo,
sessionKey: sessionRouteKey,
mainKey: sessionMainKey,
failClosedMainSessionLastRoute: failClosed,
});
These changes prevent an untrusted user from steering cron deliveries by racing/updating the shared main session’s 2. 🔵 Unbounded turnSource* routing metadata allows resource exhaustion and session/prompt poisoning
DescriptionThe new Even though overrides are gated to
Vulnerable code (no length/bounds validation before using attacker-provided values): const turnSourceTo =
allowTurnSourceOverride && typeof p.turnSourceTo === "string" && p.turnSourceTo.trim()
? p.turnSourceTo.trim()
: undefined;
...
const turnSourceThreadId =
allowTurnSourceOverride && p.turnSourceThreadId != null && p.turnSourceThreadId !== ""
? p.turnSourceThreadId
: undefined;RecommendationHarden validation at the schema and handler layer:
Example (TypeBox schema hardening): const TurnSourceChannel = Type.Optional(Type.String({ maxLength: 32 }));
const TurnSourceTo = Type.Optional(Type.String({ maxLength: 256 }));
const TurnSourceAccountId = Type.Optional(Type.String({ maxLength: 128 }));
const TurnSourceThreadId = Type.Optional(
Type.Union([
Type.String({ maxLength: 128 }),
Type.Integer({ minimum: 0, maximum: 9_007_199_254_740_991 }), // JS safe int
]),
);Example (runtime validation in function assertMaxLen(name: string, v: string, max: number) {
if (v.length > max) throw new Error(`${name} too long`);
return v;
}
const turnSourceTo = allowTurnSourceOverride && typeof p.turnSourceTo === "string"
? assertMaxLen("turnSourceTo", p.turnSourceTo.trim(), 256)
: undefined;
const turnSourceThreadId = (() => {
if (!allowTurnSourceOverride || p.turnSourceThreadId == null || p.turnSourceThreadId === "") return undefined;
if (typeof p.turnSourceThreadId === "number") {
if (!Number.isFinite(p.turnSourceThreadId)) throw new Error("turnSourceThreadId must be finite");
return Math.trunc(p.turnSourceThreadId);
}
return assertMaxLen("turnSourceThreadId", p.turnSourceThreadId.trim(), 128);
})();Analyzed PR: #35451 at commit Last updated on: 2026-03-05T07:14:19Z |
Greptile SummaryThis PR canonicalizes route derivation across Key changes:
One subtle ordering detail worth being aware of: when both Additionally, Confidence Score: 4/5
Last reviewed commit: 212ebe2 |
546bac7 to
79f7791
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 79f7791b07
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…n dispatch params
|
Codex Review: Didn't find any major issues. Another round soon, please! ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
|
Addressed Aisle findings in commit 72af51d.
Validation:
|
|
@greptile codex |
|
I need more context about your question. Are you asking about:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d3f0c3c3a7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (resolved.channel && resolved.to) { | ||
| return { | ||
| status: "resolved", | ||
| reason: "ok", |
There was a problem hiding this comment.
Validate turn-source completeness before resolving fallback route
resolveSessionRouteDecision returns a resolved target immediately when resolveSessionDeliveryTarget finds any session last* route, before checking whether the caller supplied an incomplete turn-source override. If an admin chat.send request includes turnSourceTo but no valid turnSourceChannel (for example a typo that normalizes to undefined) and the session has stale channel metadata, this path silently reuses the stale recipient instead of surfacing a missing-turn-source error, which can misroute external replies to the wrong chat.
Useful? React with 👍 / 👎.
|
Closing this PR because the author has more than 10 active PRs in this repo. Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit. |
Summary
Describe the problem and fix in 2–5 bullets:
chat.send, cron delivery target resolution, and cron announce dispatch, with different fallback rules.src/infra/outbound/targets.ts;chat.sendand cron paths were migrated to that logic; explicit delivery intent semantics were applied; andAGENTS.mdwas updated with canonicalization guardrails and required parity-test coverage.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
List user-visible changes (including defaults/config).
If none, write
None.chat.sendnow accepts explicit turn-source routing fields:turnSourceChannelturnSourceToturnSourceAccountIdturnSourceThreadIdchat.senddelivery intent behavior is explicit:deliver=false=> internal onlydeliverunset => external preferred (fallback to internal when unresolved)deliver=true=> external required (returnsINVALID_REQUESTif unresolved)last*route inheritance unless explicit source/target is provided.Security Impact (required)
Yes/No) NoYes/No) NoYes/No) NoYes/No) NoYes/No) NoYes, explain risk + mitigation:Repro + Verification
Environment
mcaxtr-ubuntu6.17.0-14-genericx86_64OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1, Nodev24.13.1, pnpm10.23.0Steps
OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test src/infra/outbound/targets.test.ts src/gateway/server-methods/chat.directive-tags.test.ts src/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.tsExpected
last*routing by default.Actual
src/infra/outbound/targets.test.tssrc/gateway/server-methods/chat.directive-tags.test.tssrc/cron/isolated-agent.skips-delivery-without-whatsapp-recipient-besteffortdeliver-true.test.tsEvidence
Attach at least one:
Human Verification (required)
What you personally verified (not just CI), and how:
deliver=trueunresolved route error behaviordelivery.mode=nonedoes not attempt outbound deliveryCompatibility / Migration
Yes/No) YesYes/No) NoYes/No) NoFailure Recovery (if this breaks)
0c87950cc,41bd2e151,9dd3c6893,546bac75c).src/infra/outbound/targets.tssrc/gateway/server-methods/chat.tssrc/cron/isolated-agent/delivery-target.tssrc/cron/isolated-agent/delivery-dispatch.tssrc/cron/isolated-agent/run.tsAGENTS.mdexternal delivery unavailable:*errors when callers setdeliver=truewithout resolvable external routeRisks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write
None.AGENTS.mdguardrails + parity-test requirements for route-path changes.