Back to skill

Security audit

OpenClaw Tally

Security checks for vulnerabilities and agentic risk

Overview

This skill is a local analytics library, but its privacy documentation understates that task intent and outcome summaries can be persisted in plaintext.

Review this before installing or integrating. It does not appear to exfiltrate data or auto-register hooks, but any hook that feeds it messages should disclose that it may store task intent, outcomes, session IDs, models, costs, and cron metadata locally. Prefer using the default database path, restrict file permissions, avoid passing raw user messages into summary fields, add retention/deletion controls, and update dependencies before running tests or CI.

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 (2)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
src/ledger.js:15
Finding

Database path restriction can be bypassed through symbolic links

Content
View full analysis

Vulnerability Details

File Location: src/ledger.js:15-29, 48-56
Vulnerability Type: Symbolic-link path traversal and filesystem boundary bypass
Risk Level: Medium

Vulnerable Code

js
/**
 * Validate that a path is within the allowed tally directory.
 * Rejects path traversal attempts.
 */
function validateDbPath(dbPath) {
  const resolved = resolve(dbPath)
  const allowed =
    resolved === TALLY_DIR ||
    resolved.startsWith(`${TALLY_DIR}${sep}`) ||
    resolved.startsWith('/tmp/') ||
    resolved.startsWith('/private/tmp/')
  if (!allowed) {
    throw new Error('Database path must be within ~/.openclaw/tally/ or /tmp/')
  }
  if (resolved.includes('..')) {
    throw new Error('Path traversal is not allowed')
  }
  return resolved
}
js
init() {
  try {
    mkdirSync(dirname(this.dbPath), { recursive: true })
    validateDbPath(this.dbPath)
    this.db = new Database(this.dbPath)
    this.db.pragma('journal_mode = WAL')

    const migrationPath = resolve(__dirname, '..', 'db', 'migrations', '001_initial.sql')
    const sql = readFileSync(migrationPath, 'utf-8')
    this.db.exec(sql)

    return this
  } catch (err) {
    throw new Error('Failed to initialize Tally database')
  }
}

Technical Analysis

validateDbPath() performs only lexical path normalization with path.resolve(). It checks whether the resulting string begins with an allowed directory prefix, but it does not resolve symbolic links or verify the canonical filesystem destination.

Consequently, a path such as /tmp/tally-link/data.db passes validation even if /tmp/tally-link is a symbolic link to a directory outside /tmp. When better-sqlite3 opens the path, the operating system follows the symbolic link and creates or modifies the database at the link target.

The secondary resolved.includes('..') check does not mitigate this issue because resolve() has a ...[truncated 1485 chars]

Remediation
View remediation

Remediation Suggestions

  1. Resolve the canonical path of the nearest existing parent directory with realpathSync() before opening the database.
  2. Verify that the canonical parent remains under a canonical allowed root using path.relative() rather than a string-prefix check.
  3. Inspect each existing path component with lstatSync() and reject symbolic links.
  4. Open or create storage using protections against symbolic-link races where the target platform supports them.
  5. Remove public support for arbitrary /tmp paths in production. Provide a separate test-only constructor or dependency-injected storage factory instead.
  6. Create a dedicated temporary directory with mkdtempSync() and restrictive permissions when temporary databases are required.
  7. Add tests covering symlinked parent directories, symlinked database files, similarly prefixed paths, and time-of-check/time-of-use replacement attempts.

A canonical containment check should follow this pattern:

js
import { realpathSync, lstatSync } from 'node:fs'
import { relative, isAbsolute } from 'node:path'

function isContained(root, targetParent) {
  const canonicalRoot = realpathSync(root)
  const canonicalParent = realpathSync(targetParent)
  const rel = relative(canonicalRoot, canonicalParent)
  return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))
}

The application must additionally reject symbolic-link components and account for race conditions between validation and opening the file.

T09 · Insecure Skill Coding Practices

Warning
Location
src/ledger.js:65
Finding

Plaintext task content is persisted despite metadata-only privacy claims

Content
View full analysis

Vulnerability Details

File Location: src/ledger.js:65-88, 105-125, 180-199; db/migrations/001_initial.sql:22-23
Vulnerability Type: Plaintext sensitive-data storage and inaccurate privacy boundary
Risk Level: Medium

Vulnerable Code

js
startTask(taskId, metadata = {}) {
  try {
    const stmt = this.db.prepare(`
      INSERT INTO tasks (task_id, parent_task_id, started_at, status, complexity_score, complexity_level,
        models_used, sessions, cron_id, cron_triggered, intent_summary)
      VALUES (@task_id, @parent_task_id, @started_at, 'in_progress', @complexity_score, @complexity_level,
        @models_used, @sessions, @cron_id, @cron_triggered, @intent_summary)
    `)

    stmt.run({
      task_id: taskId,
      parent_task_id: metadata.parent_task_id || null,
      started_at: metadata.started_at || new Date().toISOString(),
      complexity_score: metadata.complexity_score || 0,
      complexity_level: metadata.complexity_level || 'L1',
      models_used: JSON.stringify(metadata.models_used || []),
      sessions: JSON.stringify(metadata.sessions || []),
      cron_id: metadata.cron_id || null,
      cron_triggered: metadata.cron_triggered ? 1 : 0,
      intent_summary: metadata.intent_summary || '',
    })
  } catch (err) {
    throw new Error('Failed to create task')
  }
}
js
const allowed = [
  'status', 'complexity_score', 'complexity_level', 'quality_score',
  'total_tokens', 'total_cost_usd', 'tes', 'sub_agents',
  'tools_called', 'tool_names', 'external_api_calls', 'user_turns',
  'intent_summary', 'outcome_summary',
]

const setClauses = []
const params = { task_id: taskId }

for (const key of allowed) {
  if (key in updates) {
    const val = updates[key]
    setClauses.push(`${key} = @${key}`)
    params[key] = Array.isArray(val) ? JSON.stringify(val) : val
  }
}
js
completeTask(taskId, qualityScore, outco
...[truncated 3414 chars]
Remediation
View remediation

Remediation Suggestions

  1. Remove intent_summary and outcome_summary from persistent storage if the metadata-only privacy guarantee is intended to remain accurate.
  2. If summaries are required, make collection explicitly opt-in and update all documentation to identify every persisted field.
  3. Prohibit raw message storage at the API boundary and apply deterministic redaction for secrets, credentials, personal data, file paths, and identifiers.
  4. Enforce strict maximum lengths and validated schemas before every ledger write. The existing Zod schema should be integrated into startTask(), updateTask(), and completeTask() rather than remaining optional.
  5. Create ~/.openclaw/tally/ with mode 0700 and the database with mode 0600, then verify existing permissions during initialization.
  6. Define configurable retention limits and provide deletion and database-compaction functionality.
  7. Document that SQLite WAL and SHM files may contain the same sensitive records and must receive equivalent permission and retention controls.
  8. Consider field-level encryption if semantic summaries must be retained. Encryption keys should be held separately from the database and obtained from an operating-system credential store.
  9. Add tests proving that raw messages cannot be persisted accidentally and that disabled summary collection leaves no task content in the database, WAL files, or backups.
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (21)

Hidden Instructions

High
Category
Prompt Injection
Confidence
70% confidence
Finding

Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Content

Scanner excerpt · PRD.md (reported line 50)May include surrounding context.

md
- Users can answer "what did my AI spend money on this week?" in under 10 seconds
- Model routing decisions improve TES by **≥20%** within 3 months of deployment

<!-- 核心洞见:用户不关心 token,关心的是「完成了什么事、花了多少钱」。
     这个系统把 OpenClaw 从「Token 计量器」变成「任务完成引擎」。 -->

---

Hidden Instructions

High
Category
Prompt Injection
Confidence
70% confidence
Finding

Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Content

Scanner excerpt · PRD.md (reported line 50)May include surrounding context.

md
- Users can answer "what did my AI spend money on this week?" in under 10 seconds
- Model routing decisions improve TES by **≥20%** within 3 months of deployment

<!-- 核心洞见:用户不关心 token,关心的是「完成了什么事、花了多少钱」。
     这个系统把 OpenClaw 从「Token 计量器」变成「任务完成引擎」。 -->

---

Hidden Instructions

High
Category
Prompt Injection
Confidence
70% confidence
Finding

Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Content

Scanner excerpt · PRD.md (reported line 314)May include surrounding context.

md
### Boundary Cases & Rules

<!-- 边界案例很重要:这些是系统容易出错的地方。 -->

| Scenario | Resolution |
|----------|------------|

Hidden Instructions

High
Category
Prompt Injection
Confidence
70% confidence
Finding

Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Content

Scanner excerpt · PRD.md (reported line 314)May include surrounding context.

md
### Boundary Cases & Rules

<!-- 边界案例很重要:这些是系统容易出错的地方。 -->

| Scenario | Resolution |
|----------|------------|

Hidden Instructions

High
Category
Prompt Injection
Confidence
70% confidence
Finding

Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Content

Scanner excerpt · PRD.md (reported line 449)May include surrounding context.

md
| **Idle Ratio** | tokens_outside_tasks / total_tokens | How much is "overhead" |
| **Cost per Task** | session_total_cost / tasks_completed | Average task cost in session |

<!-- Session 分析的价值:找出"闲聊型"session(高 token、低任务密度)和"高效型"session。 -->

### Dimension 3: Cron Efficiency

Hidden Instructions

High
Category
Prompt Injection
Confidence
70% confidence
Finding

Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Content

Scanner excerpt · PRD.md (reported line 449)May include surrounding context.

md
| **Idle Ratio** | tokens_outside_tasks / total_tokens | How much is "overhead" |
| **Cost per Task** | session_total_cost / tasks_completed | Average task cost in session |

<!-- Session 分析的价值:找出"闲聊型"session(高 token、低任务密度)和"高效型"session。 -->

### Dimension 3: Cron Efficiency

Known Vulnerable Dependency: nanoid==3.3.17 — 1 advisory(ies): CVE-2026-67213 (nanoid: custom generators can loop indefinitely when size is zero)

High
Category
Supply Chain
Confidence
80% confidence
Finding

nanoid 3.3.17 is reported as vulnerable because custom generators can loop indefinitely when called with size zero, creating a denial-of-service condition. In this lockfile it appears only as a transitive dev dependency of postcss/vite rather than a direct runtime dependency, so the practical exposure is limited unless build tooling invokes the affected API with attacker-controlled parameters.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The PRD describes automatic collection of per-message/task metadata, lineage, and generated summaries, but does not pair this with an explicit user-facing notice, consent model, or privacy controls. In a system tracking cross-session behavior, cron activity, models used, and intent/outcome summaries, silent collection increases the risk of unexpected surveillance, regulatory noncompliance, and retention of sensitive behavioral data.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The document specifies trigger examples like "帮我…" and rejection text like "不对", "重来" as part of task classification behavior. This bakes specific locale assumptions into natural-language detection, but the PRD does not state that the system is Chinese-only or that users can configure or choose supported languages.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The document states a privacy guardrail of 'No task content stored — only metadata', but the schema includes intent_summary and outcome_summary, which are derived from conversation/task content and therefore constitute content storage. This mismatch can mislead implementers and users into believing sensitive data is not retained when, in practice, summaries may preserve confidential details or personal information.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The docstring says TES = quality_score / (normalized_cost × complexity_weight) and defines complexity_weight = 1 / (1 + ln(complexity_score + 1)). But the implementation divides by complexityWeight, which mathematically multiplies the score by (1 + ln(...)) rather than down-weighting higher complexity. This is an active contradiction between the documented formula and the code's actual effect.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The detector's natural-language behavior is hard-coded around English and Chinese regex patterns for task start, completion, and failure. This creates a locale/language policy issue because users communicating in other languages are implicitly unsupported, and the file does not offer any language choice or justification for the restriction.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The doc comment says the optional dbPath is 'Ignored for security; always uses hardcoded path,' which directly contradicts the implementation. When dbPath is provided, the constructor validates it and assigns it to this.dbPath, allowing alternate database locations under ~/.openclaw/tally or /tmp/.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
86% confidence
Finding

The skill initializes a persistent SQLite database under the user's home directory and immediately runs migrations, creating or modifying local files. Although the code has developer comments, it lacks user-facing disclosure such as a prompt, log, or notice that task data will be stored on disk, and later methods store metadata like sessions and intent summaries there.

Content

No source excerpt is available for this finding.

Known Vulnerable Dependency: @vitest/mocker==4.1.10 — 1 advisory(ies): CVE-2026-84373 (Vitest: Path Traversal / Arbitrary File Read via @vitest/mocker Redirect Mock)

Low
Category
Supply Chain
Confidence
93% confidence
Finding

@vitest/mocker 4.1.10 is flagged for a path traversal/arbitrary file read issue in redirect-mock handling. Although it is a dev-only testing dependency and not part of the runtime package surface, it can still affect developers or CI if untrusted test inputs, fixtures, or mock configuration are processed during test execution.

Content

No source excerpt is available for this finding.

Known Vulnerable Dependency: vitest==4.1.10 — 1 advisory(ies): CVE-2026-84373 (Vitest: Path Traversal / Arbitrary File Read via @vitest/mocker Redirect Mock)

Low
Category
Supply Chain
Confidence
91% confidence
Finding

vitest 4.1.10 includes the same vulnerable mocker component affected by CVE-2026-84373, so the package-level finding is valid. Because vitest is a devDependency, the issue is primarily relevant in development and CI rather than in deployed production use of this skill, which lowers but does not eliminate risk.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Content

Scanner excerpt · package.json (reported line 21)May include surrounding context.

json
"migrate": "node -e \"import('./src/ledger.js').then(m => new m.TaskLedger().init())\""
  },
  "dependencies": {
    "better-sqlite3": "^13.0.2",
    "zod": "^4.4.3"
  },
  "devDependencies": {

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Content

Scanner excerpt · package.json (reported line 22)May include surrounding context.

json
},
  "dependencies": {
    "better-sqlite3": "^13.0.2",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "vitest": "^4.1.10"

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
40% confidence
Finding

Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Content

Scanner excerpt · package.json (reported line 25)May include surrounding context.

json
"zod": "^4.4.3"
  },
  "devDependencies": {
    "vitest": "^4.1.10"
  },
  "engines": {
    "node": ">=22"

Known Vulnerable Dependency: vitest==4.1.10 — 1 advisory(ies): CVE-2026-84373 (Vitest: Path Traversal / Arbitrary File Read via @vitest/mocker Redirect Mock)

Low
Category
Supply Chain
Confidence
88% confidence
Finding

vitest 4.1.10 is flagged with a path traversal/arbitrary file read advisory in @vitest/mocker. Although this is a devDependency and not part of normal runtime use, it can still affect developer or CI environments if untrusted test code, mock redirect inputs, or attacker-controlled test fixtures are processed.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
89% confidence
Finding

This code includes natural-language examples such as "今天天气怎样" and "帮我查一下天气", implying behavior validated against a specific language. Under the policy rule, forcing or assuming a specific language without opt-in or documented justification can be a locale-policy issue.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.