Back to skill

Security audit

Session History Enhanced

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly a coherent session-history feature, but its sample implementation has real risks around unscoped agent IDs, internal path exposure, and misleading deletion behavior for chat transcripts.

Review this before installing, especially if OpenClaw is exposed to multiple users or untrusted dashboard clients. The feature is useful and not clearly malicious, but it should validate agent IDs against configured agents, avoid returning internal file paths, make archive operations transactional or fail closed, and align deletion prompts with actual transcript deletion before being trusted with private chat history.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
references/INSTALL.md:56
Finding
Caller-Controlled Agent ID Enables Path Traversal Outside the Agent Directory<![CDATA[ ## Vulnerability Details **File Location**: `references/INSTALL.md:56-60`; `references/backend-protocol-schemas.ts.txt:14-59`; `references/backend-rpc-handlers.ts.txt:90-96, 119-128, 162-168, 184-190`; `references/backend-history-migration.ts.txt:29-40, 68-114, 197-204` **Vulnerability Type**: Path traversal and insufficient authorization validation **Risk Level**: High ### Complete Code Snippet The documented path helper places an unvalidated agent identifier directly into a filesystem path: ```ts export function resolveSessionTranscriptsDirForAgent(agentId?: string): string { const home = process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw"); const id = agentId || "main"; return path.join(home, "agents", id, "sessions"); } ``` The RPC schemas only require a non-empty string: ```ts export const SessionsArchivedParamsSchema = Type.Object( { agentId: Type.Optional(NonEmptyString), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })), offset: Type.Optional(Type.Integer({ minimum: 0 })), search: Type.Optional(Type.String()), status: Type.Optional(Type.Union([Type.Literal("active"), Type.Literal("archived")])), }, { additionalProperties: false }, ); export const SessionsResumeParamsSchema = Type.Object( { sessionId: NonEmptyString, agentId: Type.Optional(NonEmptyString), }, { additionalProperties: false }, ); export const SessionsRenameParamsSchema = Type.Object( { sessionId: NonEmptyString, displayName: Type.String(), agentId: Type.Optional(NonEmptyString), }, { additionalProperties: false }, ); export const SessionsDeleteArchivedParamsSchema = Type.Object( { sessionId: NonEmptyString, agentId: Type.Optional(NonEmptyString), deleteTranscript: Type.Optional(Type.Boolean()), }, { additionalProperties: false }, ); ``` The supplied value is used without checking that it identifies a configured agent or remains beneath the expected directory: ``` ...[truncated 3935 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not derive filesystem paths directly from an RPC-provided agent identifier. 2. Resolve the identifier against a trusted list of configured agents and reject unknown IDs. 3. Restrict agent identifiers to a conservative allowlist, such as letters, numbers, underscores, and hyphens. 4. Explicitly reject `/`, `\`, `.`, `..`, absolute paths, null bytes, and platform-specific path separators. 5. Canonicalize and verify containment before any filesystem access: ```ts const agentsRoot = path.resolve(home, "agents"); const candidate = path.resolve(agentsRoot, validatedAgentId, "sessions"); const relative = path.relative(agentsRoot, candidate); if ( relative === "" || relative.startsWith("..") || path.isAbsolute(relative) ) { throw new Error("Invalid agent directory"); } ``` 6. Enforce per-agent authorization in every affected RPC handler. 7. Run migration only for directories derived from trusted server-side configuration. 8. Add tests covering `../`, absolute paths, backslash traversal, encoded separators, unknown agents, and cross-agent requests. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
references/backend-history-db.ts.txt:147
Finding
Archive Listing Exposes Internal Transcript File Paths<![CDATA[ ## Vulnerability Details **File Location**: `references/backend-history-db.ts.txt:4-18, 147-154`; `references/backend-rpc-handlers.ts.txt:90-106` **Vulnerability Type**: Sensitive server-path information disclosure **Risk Level**: Medium ### Complete Code Snippet The internal database row includes the transcript path: ```ts export type SessionHistoryRow = { sessionId: string; agentId: string; sessionKey: string; displayName?: string; createdAt: number; updatedAt: number; archivedAt?: number; messageCount: number; filePath: string; firstMessage?: string; channel?: string; chatType: string; totalTokens?: number; status: "active" | "archived"; }; ``` The listing query returns every column: ```ts const stmt = db.prepare(` SELECT * FROM session_history ${whereClause} ORDER BY updatedAt DESC LIMIT ? OFFSET ? `); const sessions = stmt.all(...params, limit, offset) as SessionHistoryRow[]; return { sessions, total }; ``` The RPC forwards the database result directly to the client: ```ts const result = listArchivedSessions(historyDb, { agentId: resolvedAgentId, limit, offset, search, status, }); respond(true, result, undefined); ``` ### Technical Analysis The archive API uses the internal persistence model as its public response object. Since `SELECT *` includes `filePath`, clients receive the server-side location of each transcript. The frontend only needs session metadata and does not need the absolute storage path. This violates data minimization and leaks details about the server's filesystem layout. Returning database rows directly also creates a maintenance risk because future internal columns will automatically become externally visible. ### Attack Path 1. An attacker or low-privileged client obtains access to the `sessions.archived` RPC. 2. The client requests archived sessions. 3. The database executes `SELECT *`. 4. The handler returns each complete `SessionHistoryRow`. 5. The client extracts ...[truncated 602 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define a dedicated public response type that excludes `filePath`. 2. Select only fields required by the frontend rather than using `SELECT *`: ```ts const stmt = db.prepare(` SELECT sessionId, agentId, sessionKey, displayName, createdAt, updatedAt, archivedAt, messageCount, firstMessage, channel, chatType, totalTokens, status FROM session_history ${whereClause} ORDER BY updatedAt DESC LIMIT ? OFFSET ? `); ``` 3. Map database rows to an explicit RPC DTO before calling `respond()`. 4. Add response-schema validation so internal fields cannot be exposed accidentally. 5. Review whether `firstMessage`, `sessionKey`, and other metadata should be visible to every caller, and enforce appropriate authorization. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
references/backend-session-archive.ts.txt:169
Finding
Fail-Open Archival Can Remove an Active Session After Archival Failure<![CDATA[ ## Vulnerability Details **File Location**: `references/backend-session-archive.ts.txt:169-196`; `references/backend-rpc-handlers.ts.txt:58-79` **Vulnerability Type**: Non-atomic destructive operation and inconsistent error handling **Risk Level**: Medium ### Complete Code Snippet The archive implementation suppresses transcript-move or database-indexing errors: ```ts try { // Move transcript file to archive directory if (currentTranscriptPath !== archiveTranscriptPath) { fs.renameSync(currentTranscriptPath, archiveTranscriptPath); } // Create history database entry const historyEntry: SessionHistoryRow = { sessionId: sessionEntry.sessionId, agentId, sessionKey: "", displayName: sessionEntry.displayName, createdAt: metadata.createdAt || Date.now(), updatedAt: metadata.updatedAt || sessionEntry.updatedAt || Date.now(), archivedAt: Date.now(), messageCount: metadata.messageCount, filePath: archiveTranscriptPath, firstMessage: metadata.firstMessage, channel: sessionEntry.lastChannel, chatType: sessionEntry.chatType || "direct", totalTokens: metadata.totalTokens, status: "archived", }; indexSession(historyDb, historyEntry); } catch (error) { // If move fails, log but don't throw - we don't want to break session creation console.error(`Failed to archive session ${sessionEntry.sessionId}:`, error); } ``` The RPC handler then removes the active session entry regardless of whether archival succeeded: ```ts try { const sessionsDir = resolveSessionTranscriptsDirForAgent(target.agentId); archiveSessionToHistory({ sessionEntry: entry, sessionsDir, agentId: target.agentId, reason: "archived", }); } catch (err) { // Log but don't fail — the session store removal is more important console.error(`Failed to index archived session ${sessionId}:`, err); } // Remove from active session store await updateSessionStore(storePath, (store) => { const { primaryKey } ...[truncated 2061 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not suppress archival failures. Return a typed failure result or throw an exception. 2. Remove the active store entry only after the transcript move and database update both succeed. 3. Implement compensating rollback: - If the database write fails after the move, move the transcript back. - If active-store removal fails, restore the prior file and database state where possible. 4. Use a temporary destination followed by an atomic rename after metadata is committed. 5. Detect destination collisions explicitly rather than overwriting or failing ambiguously. 6. Return an RPC error when archival is incomplete; never report `archived: true` for partial failure. 7. Record operation IDs and structured logs so interrupted operations can be reconciled. 8. Add fault-injection tests for rename failures, database failures, disk-full conditions, collisions, and active-store update failures. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
references/frontend-controllers-sessions.ts.txt:250
Finding
Permanent Delete Workflow Retains Transcript Files Contrary to User Confirmation<![CDATA[ ## Vulnerability Details **File Location**: `references/frontend-controllers-sessions.ts.txt:250-270`; `references/backend-rpc-handlers.ts.txt:184-207` **Vulnerability Type**: Sensitive data retention caused by frontend/backend parameter mismatch **Risk Level**: Medium ### Complete Code Snippet The frontend tells the user that the transcript will be deleted, but it does not request transcript deletion: ```ts export async function deleteArchivedSession( state: SessionsState, sessionId: string, ): Promise<boolean> { if (!state.client || !state.connected) { return false; } const confirmed = window.confirm( `Delete archived session?\n\nThis will permanently delete the session and its transcript.`, ); if (!confirmed) { return false; } try { await state.client.request("sessions.deleteArchived", { sessionId }); // Reload archived sessions to reflect the change await loadArchivedSessions( state, undefined, state.archivedSessionsSearch || undefined, state.archivedSessionsPageSize, (state.archivedSessionsPage - 1) * state.archivedSessionsPageSize, ); return true; } catch (err) { state.archivedSessionsError = String(err); return false; } } ``` The backend defaults `deleteTranscript` to `false`: ```ts const { sessionId, agentId, deleteTranscript = false } = params; const config = loadConfig(); const resolvedAgentId = agentId ?? resolveDefaultAgentId(config); try { const sessionsDir = resolveSessionTranscriptsDirForAgent(resolvedAgentId); const historyDb = initHistoryDbWithMigration(sessionsDir, resolvedAgentId); const sessionRecord = historyDb .prepare(`SELECT * FROM session_history WHERE sessionId = ?`) .get(sessionId) as unknown; if (!sessionRecord) { respond(false, undefined, errorShape(ErrorCodes.NotFound, "Session not found", false)); return; } const filePath = (sessionRecord as { filePath?: string }).filePath; if (deleteTranscrip ...[truncated 1824 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make the frontend request match the confirmation: ```ts await state.client.request("sessions.deleteArchived", { sessionId, deleteTranscript: true, }); ``` 2. Alternatively, present an explicit user choice between deleting only the history record and deleting both the record and transcript. 3. Consider requiring an explicit `deleteTranscript` value in the RPC schema instead of silently defaulting it. 4. Delete the database record only after transcript deletion succeeds. 5. If `unlinkSync()` fails, return an error and retain the database record so the transcript remains discoverable and recoverable. 6. Report `transcriptDeleted` based on successful deletion rather than merely on the presence of a requested path. 7. Add tests verifying that confirmed permanent deletion removes both the database entry and the transcript file. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (12)

Ae1

High
Category
analysis-evasion
Content
| `src/config/sessions/history-db.ts` | [references/backend-history-db.ts.txt](references/backend-history-db.ts.txt) | SQLite CRUD operations |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `ui/src/ui/views/sessions.ts` | [references/frontend-views-sessions.ts.txt](references/frontend-views-sessions.ts.txt) | Full view with Session History sectio
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The markdown describes `sessions.deleteArchived` as deleting archived sessions with an optional transcript deletion, which can affect user data. The file does not include any warning, confirmation note, or caution about irreversible data loss or the privacy implications of removing stored transcripts.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
This markdown installation guide introduces a `sessions.deleteArchived` capability, which is a destructive operation affecting user data. The document does not provide any warning about deletion risk, reversibility, or need for confirmation, despite describing user-facing Delete buttons elsewhere in the file.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This code moves transcript files into and out of an archive using fs.renameSync, which changes the user's session file state and can affect data availability. While there is error logging on failure, there is no confirmation prompt, user-facing disclosure, or explanatory comment/docstring warning that these operations relocate session transcripts.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The confirmation dialog says the archived session deletion will permanently delete the session and transcript, but the actual request only sends { sessionId } with no explicit transcript-delete flag or visible guarantee that the backend performs the same action. Mismatched destructive-action messaging can cause users to authorize operations under false assumptions, leading to unintended data retention or deletion and weakening trust and auditability around sensitive chat history management.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This code provides destructive deletion buttons for archived sessions, and similarly for live sessions, without any confirmation prompt, warning text, or inline disclosure about permanence. For code files, destructive or irreversible operations should have some visible user disclosure unless clearly covered elsewhere, which is not evident in this file.

Intent-Code Divergence

Low
Confidence
95% confidence
Finding
The documentation says this function lists archived sessions, but the implementation accepts an optional status filter and will return active sessions if opts.status is set to "active". This is an intent/documentation mismatch because the comment narrows behavior more than the code actually enforces.

Missing User Warnings

Low
Confidence
81% confidence
Finding
This code makes browser client requests to list sessions, including archived sessions metadata, but the function contains no confirmation, log, print, or explanatory comment disclosing that session data will be transmitted or fetched. For code files, user/system data network operations can warrant a warning unless the disclosure exists elsewhere or is clearly part of the stated skill purpose; no such disclosure is visible in this file.

Missing User Warnings

Low
Confidence
77% confidence
Finding
The patch operation transmits session identifiers and user-supplied updates to the backend, but there is no user-facing notice, print/log, or explanatory comment describing that remote session state will be changed. Although modification may be expected in context, that expectation is not documented within this file.

Missing User Warnings

Low
Confidence
74% confidence
Finding
Both resumeSession and renameSession issue backend requests that alter session state, but unlike the delete flows they provide no confirmation, visible notice, or explanatory comment. For code files, safety-relevant remote operations should have at least some disclosure when not otherwise documented.

Missing User Warnings

Low
Confidence
79% confidence
Finding
Archiving changes the state and location of a session, but the UI only provides a tooltip-like title and no explicit disclosure about what the operation does beyond moving to history. Because this is a state-changing operation affecting user session data, a clearer warning or explanatory text would improve safety and user awareness.

Static analysis

No suspicious patterns detected.