T09 · Insecure Skill Coding Practices
- Location
references/observability-lab-source/hooks/telemetry.ts:180- Finding
Unredacted Sensitive Telemetry Stored in Unbounded Plaintext Logs
- Content
View full analysis
Vulnerability Details
File Location:
references/observability-lab-source/hooks/telemetry.ts:180-186, 204-210, 231-237, 259-265;references/observability-lab-source/src/observability.ts:175-196
Vulnerability Type: Plaintext storage of potentially sensitive telemetry without redaction, restrictive permissions, or retention controls
Risk Level: MediumVulnerable Code
ts enqueueTelemetryEvent({ stateDir: api.runtime.state.resolveStateDir(), record: { type, at: new Date().toISOString(), event: { ...event }, context: { ...context }, }, });The same complete-event capture pattern is used for message, LLM, and tool events. Assistant transcript messages are also captured:
ts enqueueTelemetryEvent({ stateDir: api.runtime.state.resolveStateDir(), record: { type: "before_message_write", at: new Date().toISOString(), event: { message: event.message as Record<string, unknown> }, context: { ...context }, }, });Queued records are serialized and appended to a persistent JSONL file:
ts function startTelemetryFlush(logPath: string, queue: TelemetryQueue): void { if (queue.flushPromise) { return; } queue.flushPromise = Promise.resolve() .then(async () => { await fs.mkdir(path.dirname(logPath), { recursive: true }); // Drain in batches so a burst of hook events still collapses into a small number of writes. while (queue.pendingLines.length > 0) { const lines = queue.pendingLines.splice(0, queue.pendingLines.length); await fs.appendFile(logPath, lines.join(""), "utf8"); } }) .finally(() => { queue.flushPromise = null; if (queue.pendingLines.length > 0) { startTelemetryFlush(logPath, queue); return; } if (telemetryQueues.get(logPath) === queue) { telemetryQueues.delete(logPath); } }); }Technical Analysis
The observability hooks copy complete event and context object ...[truncated 2866 chars]
- Remediation
View remediation
Remediation Suggestions
-
Use an explicit field allowlist
- Store only operational metadata required for diagnostics, such as event type, timestamp, duration, model identifier, and success status.
- Do not serialize complete event or context objects.
-
Redact sensitive content
- Remove authorization headers, cookies, API keys, access tokens, passwords, private keys, and credential-like values.
- Redact prompt text, user content, tool arguments, and tool results by default.
- Apply recursive redaction before records enter the in-memory queue.
-
Separate metadata and content capture
- Keep metadata-only telemetry as the default.
- Require a distinct, explicit opt-in for full-content recording.
- Clearly warn that full-content capture may retain secrets and personal data.
-
Restrict filesystem permissions
- Create the plugin directory with mode
0o700. - Create the telemetry file with mode
0o600. - Validate existing file ownership and permissions before appending.
- Create the plugin directory with mode
-
Implement retention controls
- Add configurable maximum file size and record age.
- Rotate logs and cap the number of retained files.
- Provide an authenticated purge command and document secure deletion behavior.
-
Reduce activation scope
- Make activation explicit rather than activating from any plugin interaction.
- Display a visible recording indicator while capture is active.
- Allow immediate per-session deactivation without changing the global configuration.
-
Handle disk and serialization failures safely
- Limit queue size to prevent unbounded memory growth during filesystem failures.
- Catch write failures and degrade telemetry without crashing unrelated plugin functionality.
- Avoid recursively logging telemetry errors containing sensitive records.
-
