fix(agents): harden cli runner hook followups#70747
Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟠 Symlink/hardlink traversal allows reading arbitrary local files via CLI session history loader
Description
Vulnerable code: const stat = fs.statSync(sessionFile);
if (!stat.isFile() || stat.size > MAX_CLI_SESSION_HISTORY_FILE_BYTES) {
return [];
}
const entries = SessionManager.open(sessionFile).getEntries();RecommendationHarden transcript file access against symlink/hardlink attacks. Minimum mitigation (symlink refusal):
Example: import path from "node:path";
const st = fs.lstatSync(sessionFile);
if (!st.isFile() || st.isSymbolicLink()) return [];
const real = fs.realpathSync(sessionFile);
const sessionsDir = path.resolve(path.dirname(sessionFile)); // or the resolved sessions root
if (!real.startsWith(sessionsDir + path.sep)) return [];Stronger mitigation (TOCTOU-resistant):
2. 🟠 Cross-agent session transcript disclosure via user-controlled agentId/sessionKey in CLI session history loader
Description
Impact:
Vulnerable code: const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
...
resolveSessionFilePath(..., resolveSessionFilePathOptions({
agentId: sessionAgentId ?? defaultAgentId,
}))RecommendationBind transcript resolution to an authenticated/authorized agent scope rather than caller-provided identifiers. Options:
Example hardening (prefer sessionKey-derived scope and disallow override): const parsed = params.sessionKey ? parseAgentSessionKey(params.sessionKey) : null;
if (!parsed) throw new Error("Missing/invalid sessionKey");
const sessionAgentId = normalizeAgentId(parsed.agentId);
return resolveSessionFilePath(
params.sessionId,
{ sessionFile: params.sessionFile },
resolveSessionFilePathOptions({ agentId: sessionAgentId }),
);If you must support Analyzed PR: #70747 at commit Last updated on: 2026-04-23T19:32:24Z |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c8edc265dc
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| resolveSessionFilePathOptions({ | ||
| agentId: sessionAgentId ?? defaultAgentId, | ||
| }), |
There was a problem hiding this comment.
Respect custom session store when resolving CLI history
When cfg.session.store points to a non-default location, session transcripts are resolved relative to that store directory, but this helper builds path options with only agentId and omits storePath. In that case resolveSessionFilePath(...) treats the real sessionFile as out-of-scope and falls back to the default state transcript path, so hook history becomes empty or stale for before_prompt_build, llm_input, and agent_end events in custom-store deployments.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR fixes two hook-lifecycle bugs in the CLI runner (duplicate Confidence Score: 5/5Safe to merge; all remaining findings are P2 observations. The two targeted bugs are correctly fixed and well-covered by new regression tests. The one flagged concern (structural-path fallback in resolveSessionFilePath potentially bypassing canonical-path selection) is a P2 gap in test coverage rather than a present defect — the caller-supplied sessionFile comes from trusted PreparedCliRunContext, not from user input. src/agents/cli-runner/session-history.ts — verify behavior of resolveStructuralSessionFallbackPath for external paths with agents//sessions/ structure Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/cli-runner/session-history.ts
Line: 46-52
Comment:
**Structural-path fallback may bypass canonical-path restriction**
`resolveSessionFilePath` (in `paths.ts`) includes a `resolveStructuralSessionFallbackPath` branch that returns the caller-supplied `sessionFile` as-is when it has the directory shape `…/agents/<agentId>/sessions/<filename>` — even if that path lives outside `OPENCLAW_STATE_DIR`. This means a caller-supplied path like `/attacker/agents/main/sessions/stolen.jsonl` would satisfy the structural check and be returned verbatim, defeating the canonical-path hardening introduced here.
The existing tests cover the case where the external path lacks that directory structure (a flat `stolen.jsonl`). Consider adding a test where the external path *does* match the `agents/<id>/sessions/<file>` shape to confirm the restriction holds.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(agents): harden cli runner hook foll..." | Re-trigger Greptile |
| const stat = fs.statSync(sessionFile); | ||
| if (!stat.isFile() || stat.size > MAX_CLI_SESSION_HISTORY_FILE_BYTES) { | ||
| return []; | ||
| } | ||
| const entries = SessionManager.open(sessionFile).getEntries(); | ||
| const history: unknown[] = []; | ||
| for (let index = entries.length - 1; index >= 0; index -= 1) { |
There was a problem hiding this comment.
Structural-path fallback may bypass canonical-path restriction
resolveSessionFilePath (in paths.ts) includes a resolveStructuralSessionFallbackPath branch that returns the caller-supplied sessionFile as-is when it has the directory shape …/agents/<agentId>/sessions/<filename> — even if that path lives outside OPENCLAW_STATE_DIR. This means a caller-supplied path like /attacker/agents/main/sessions/stolen.jsonl would satisfy the structural check and be returned verbatim, defeating the canonical-path hardening introduced here.
The existing tests cover the case where the external path lacks that directory structure (a flat stolen.jsonl). Consider adding a test where the external path does match the agents/<id>/sessions/<file> shape to confirm the restriction holds.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/cli-runner/session-history.ts
Line: 46-52
Comment:
**Structural-path fallback may bypass canonical-path restriction**
`resolveSessionFilePath` (in `paths.ts`) includes a `resolveStructuralSessionFallbackPath` branch that returns the caller-supplied `sessionFile` as-is when it has the directory shape `…/agents/<agentId>/sessions/<filename>` — even if that path lives outside `OPENCLAW_STATE_DIR`. This means a caller-supplied path like `/attacker/agents/main/sessions/stolen.jsonl` would satisfy the structural check and be returned verbatim, defeating the canonical-path hardening introduced here.
The existing tests cover the case where the external path lacks that directory structure (a flat `stolen.jsonl`). Consider adding a test where the external path *does* match the `agents/<id>/sessions/<file>` shape to confirm the restriction holds.
How can I resolve this? If you propose a fix, please make it concise.* fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups
* fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups
* fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups
* fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups
* fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups
* fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups * fix(agents): harden cli runner hook followups
summary
llm_inputonce for a logical CLI run and always emitagent_endwhen the session-expired retry failsvalidation
pnpm exec vitest run --config test/vitest/vitest.agents.config.ts src/agents/cli-runner/session-history.test.tspnpm exec vitest run --config test/vitest/vitest.agents.config.ts src/agents/cli-runner.reliability.test.tspnpm exec vitest run --config test/vitest/vitest.agents.config.ts src/agents/cli-runner/prepare.test.tspnpm buildnote