T09 · Insecure Skill Coding Practices
- Location
assets/extension-template/index.ts:219- Finding
Cross-session LLM usage metadata disclosure
- Content
View full analysis
Vulnerability Details
File Location:
assets/extension-template/index.ts, lines 219-231
Vulnerability Type: Cross-session information disclosure caused by global usage correlation
Risk Level: MediumVulnerable Code
typescript if (!event.content || typeof event.content !== "string") return; const usage = latestUsage(); const quota = await fetchCodexQuota(quotaCacheMs); const label = modelLabel(usage?.provider, usage?.model); const win = contextWindow(usage?.model, usage?.provider); const total = usage?.total ?? ((usage?.input ?? 0) + (usage?.output ?? 0)); const effectiveTotal = Math.max(0, total + reserve); const ctxPct = win ? Math.round((effectiveTotal / win) * 100) : undefined; const status = worst(colorForUsage(ctxPct), colorForRemaining(quota?.percent)); const parts = [ `${status} ↑${fmt(usage?.input)} ↓${fmt(usage?.output)}`, `${Number.isFinite(ctxPct) ? ctxPct : "n/a"}%ctx` ]; if (quota?.percent != null) parts.push(`${quota.resetLabel ?? "5h"} ${quota.percent}%`); parts.push(label); const sub = cfg.appendSubagents === false ? null : aggregateSubagents(usage, cacheMs);The underlying fallback and aggregation logic is:
typescript function latestUsage(sessionKey?: string): Usage | undefined { if (sessionKey && recentBySession.has(sessionKey)) return recentBySession.get(sessionKey); return recentOutputs.at(-1); } function aggregateSubagents(root?: Usage, cacheMs = 120000): { input: number; output: number } | null { if (!root) return null; const cutoff = Date.now() - cacheMs; let input = 0; let output = 0; for (const u of recentOutputs) { if (u === root || u.ts < cutoff) continue; if (Math.abs(root.ts - u.ts) > cacheMs) continue; if (u.sessionKey && root.sessionKey && u.sessionKey === root.sessionKey) continue; input += u.input ?? 0; output += u.output ?? 0; } return input || outpu ...[truncated 3521 chars]- Remediation
View remediation
Remediation Suggestions
-
Pass the current session identity into the lookup and do not fall back to the globally latest output:
typescript const usage = latestUsage(ctx.sessionKey); if (!usage) return; -
Change
latestUsage()to fail closed when no session key or exact session match exists:typescript function latestUsage(sessionKey?: string): Usage | undefined { if (!sessionKey) return undefined; return recentBySession.get(sessionKey); } -
Do not infer subagent relationships solely from timestamps. Disable
appendSubagentsby default until OpenClaw supplies trustworthy parent/root run identifiers. -
When parent-run metadata is available, store it with each usage record and aggregate only records whose root-run ID, tenant identity, and owning session match the current delivery context.
-
Partition all caches by an appropriate security boundary, such as tenant plus session, rather than maintaining a process-global list usable by every conversation.
-
Remove stale entries from both
recentOutputsandrecentBySessionafter the configured retention period to reduce residual metadata exposure. -
Add concurrency tests that interleave
llm_outputandmessage_sendingevents from multiple sessions. Verify that no footer contains model or token metrics originating from another session. -
Validate channel and conversation identifiers using normalized strings and adopt an explicit provider allowlist by default where multi-tenant gateways are used.
-
