Back to skill

Security audit

Signet Guardian

Security checks for vulnerabilities and agentic risk

Overview

This payment-guard skill is not clearly malicious, but it needs review because it can permit payments by default and its spending-limit controls are not strong enough for a financial safety gate.

Review and change the policy before relying on this skill. Set paymentsEnabled to false until explicitly configured, require confirmation for all payments if unsure, avoid concurrent payment flows near the monthly cap, protect the ledger from local modification, and prefer pinned or locally installed tooling instead of unpinned npx commands.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/signet-cli.ts:263
Finding
Concurrent preflights can exceed the monthly payment cap before recording<![CDATA[ ## Vulnerability Details **File Location**: `scripts/signet-cli.ts:263-282` and `scripts/signet-cli.ts:330-365`; the limitation is also acknowledged in `SKILL.md:21-23` and `SKILL.md:128-132` **Vulnerability Type**: Time-of-check to time-of-use race in financial authorization **Risk Level**: High ### Vulnerable Code ```ts const monthSpent = getMonthSpent(policy.currency); const totalAfter = monthSpent + amount; if (totalAfter > policy.maxPerMonth) { appendLedger({ ts: new Date().toISOString(), amount, currency, payee, purpose, status: 'denied', reason: `Would exceed monthly limit (${policy.currency} ${policy.maxPerMonth})`, ...audit, }); return { result: 'DENY', reason: `Would exceed monthly limit of ${policy.currency} ${policy.maxPerMonth} (already spent ${policy.currency} ${monthSpent.toFixed(2)} this month)`, }; } ``` The locked recording operation occurs only after the external payment is expected to have completed: ```ts async function record( amount: number, currency: string, payee: string, purpose: string, idempotencyKey?: string, callerSkill?: string ): Promise<{ ok: true } | { ok: false; error: string }> { return withLock(() => { const policy = loadPolicyFromConfigOrFile(); if (!policy) return { ok: false, error: 'Policy missing or invalid' }; if (currency !== policy.currency) { return { ok: false, error: `Policy currency is ${policy.currency}; request must use ${policy.currency}` }; } const monthSpent = getMonthSpent(policy.currency); if (monthSpent + amount > policy.maxPerMonth) { return { ok: false, error: `Would exceed monthly limit of ${policy.currency} ${policy.maxPerMonth} (already ${policy.currency} ${monthSpent.toFixed(2)} this month)`, }; } if (idempotencyKey && hasIdempotencyKey(idempotencyKey)) { return { ok: true }; // idempotent: already recorded } appendLedger({ ts: new Date().to ...[truncated 2102 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Implement an atomic authorization and settlement workflow: 1. Add an `authorize` operation that acquires the ledger lock before a payment is made. 2. Under that lock, validate the complete policy, calculate completed spending plus active reservations, and create a reservation with a unique identifier. 3. Require payment-capable callers to possess a valid reservation before transferring funds. 4. Add a `settle` operation that atomically converts the reservation into a completed ledger entry. 5. Add a `release` operation for failed or cancelled payments. 6. Give reservations a bounded expiration time and reclaim expired reservations under the same lock. 7. Bind each reservation to amount, currency, payee, caller, and idempotency key so it cannot authorize a different transaction. 8. Retain preflight only as an advisory early check; do not describe it as definitive cap enforcement. 9. Add concurrency tests in which several processes attempt payments near the monthly cap and verify that only reserved payments proceed. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/signet-cli.ts:166
Finding
Malformed ledger entries are silently ignored, causing payment-limit checks to fail open<![CDATA[ ## Vulnerability Details **File Location**: `scripts/signet-cli.ts:166-184` **Vulnerability Type**: Fail-open handling of corrupted security state **Risk Level**: Medium ### Vulnerable Code ```ts function getMonthSpent(currency: string): number { const monthKey = getCurrentMonthKey(); const lines = readLedgerLines(); return lines .map((line) => { try { const entry: LedgerEntry = JSON.parse(line); if ( entry.ts?.startsWith(monthKey) && entry.status === 'completed' && typeof entry.amount === 'number' && entry.currency === currency ) { return entry.amount; } } catch {} return 0; }) .reduce((sum, a) => sum + a, 0); } ``` ### Technical Analysis The ledger is security-critical state used to enforce the monthly cap. If a JSONL line cannot be parsed, the empty `catch` block suppresses the error and maps the entry to zero. Structurally invalid entries, entries with unexpected field types, and partially written lines are also excluded from the spending total. The implementation therefore treats an unreadable spending record as if no spending occurred. A security control should instead fail closed when the state needed to calculate authorization cannot be trusted. The ledger is not cryptographically authenticated. A local process or user that already has write access to the ledger can corrupt or modify completed entries so they are ignored by subsequent policy checks. Accidental filesystem corruption or interrupted writes can produce the same result without an attacker. ### Attack Path 1. A completed transaction exists in `references/ledger.jsonl`. 2. A local actor with write access modifies that line so it is invalid JSON, or changes a security-relevant field to an unexpected type. 3. `getMonthSpent()` catches the parsing error or rejects the field and returns zero for that line. 4. The calculated monthly expenditure becomes lower than th ...[truncated 794 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Fail closed if any non-empty ledger line is malformed or fails schema validation. 2. Return a clear policy-denial result such as `Ledger integrity validation failed`; do not silently count an invalid record as zero. 3. Validate every field used for accounting, including: - A valid ISO timestamp. - A finite, positive amount. - An expected status value. - An exact configured currency. - Correct string types for identifiers and descriptive fields. 4. Reject `NaN`, `Infinity`, and unsafe numeric values. 5. Serialize all ledger writes under the same lock, including denial records. 6. Use restrictive file and directory permissions when creating the ledger. 7. Prefer a transactional database or atomic replacement strategy if the ledger becomes authoritative. 8. Add integrity protection, such as chained hashes or authenticated records, when local tamper detection is required. 9. Add tests for truncated JSON, invalid types, duplicate entries, non-finite amounts, and interrupted writes. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
references/policy.json:1
Finding
Bundled fallback policy enables payments without explicit first-run opt-in<![CDATA[ ## Vulnerability Details **File Location**: `references/policy.json:1-9` **Vulnerability Type**: Unsafe default financial authorization configuration **Risk Level**: Medium ### Vulnerable Code ```json { "paymentsEnabled": true, "maxPerTransaction": 20, "maxPerMonth": 500, "currency": "GBP", "requireConfirmationAbove": 5, "blockedMerchants": [], "allowedMerchants": [] } ``` ### Technical Analysis The CLI loads policy from OpenClaw configuration first and then falls back to `references/policy.json`. The bundled fallback file is valid and sets `paymentsEnabled` to `true`. This conflicts with the safer disabled default returned by `defaultPolicy()` and the extension schema's `paymentsEnabled: false` default. It also weakens the documented default-deny posture: when OpenClaw configuration is absent, policy loading does not fail because the bundled permissive policy is accepted. Under this policy, payments of GBP 5 or less are returned as `ALLOW` without requiring confirmation, provided the other limits are satisfied. No merchant restrictions are enabled. ### Attack Path 1. A user installs the Skill without creating an explicit `signet.policy` in OpenClaw configuration. 2. The CLI falls back to the bundled `references/policy.json`. 3. The fallback policy enables payments automatically. 4. A payment-capable Skill requests a payment of GBP 5 or less. 5. Preflight returns `ALLOW` when the monthly and per-transaction limits are available. 6. A contract-compliant caller may execute the payment without explicit user confirmation. ### Impact Assessment No system privilege escalation occurs. The affected privilege is financial authorization delegated to payment-capable agents. Potential impact includes: - Payments being permitted before the user explicitly enables the feature. - Unconfirmed transactions of up to GBP 5 each. - Multiple transactions being authorized up to the GBP 500 monthly cap. - Payments to unrestricted merchants because b ...[truncated 144 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Change the bundled policy to `"paymentsEnabled": false`. 2. Require an explicit user action to enable payments after installation. 3. Distinguish example configuration from active configuration; do not ship a permissive example at the runtime fallback path. 4. Refuse payment authorization until policy initialization has been completed and acknowledged. 5. Consider requiring confirmation for every payment by default. 6. Display the effective policy source so users can determine whether OpenClaw configuration or the fallback file is active. 7. Add a first-run warning when a fallback policy is used. 8. Add tests verifying that a fresh installation denies every payment until explicit enablement. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (31)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill claims to be a payment guard that returns ALLOW/DENY/CONFIRM_REQUIRED and enforces policy limits, but the analyzed file only documents that behavior and provides no executable enforcement. In a payment-control context, this mismatch is dangerous because dependent payment skills may trust the guard's contract and proceed as if policy checks, caps, and recording are being enforced when they are not.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The --edit path executes whatever program is named in the EDITOR environment variable via spawnSync. In agent or automation contexts, environment variables may be attacker-influenced, so invoking signet-policy --edit can become arbitrary local command execution under the agent's privileges.

Session Persistence

Medium
Category
Rogue Agent
Content
}
```

Create or edit with:

```bash
npx tsx scripts/signet-cli.ts signet-policy --edit
Confidence
82% confidence
Finding
The documented policy edit, wizard, and migration flows imply persistent state changes to security-relevant payment controls, yet the README does not emphasize the persistence and downstream effect on future agent payment decisions. In the context of a middleware intended to gate payments, persistent configuration changes materially affect the trust boundary and can reduce protections across sessions if altered accidentally or by a misled operator.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding
npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The manifest describes this skill as payment guard middleware that performs preflight policy checks and records successful payments. This file also exposes `signet-report` for spending reports and `signet-policy` for showing, editing, and migrating policy data, which expands the skill from guard middleware into policy-management and reporting functionality.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding
The script invokes `npx -y tsx` without pinning an exact package version, so execution depends on whatever version npm resolves at runtime. If a compromised or unexpected `tsx` version is fetched, arbitrary code could run in the context of this payment-related test workflow.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding
This command again uses unpinned `npx -y tsx`, which can download and execute a non-deterministic package version at runtime. Because the script is exercising payment guard logic, compromise of the toolchain could falsify results or execute attacker-controlled code during validation.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding
An unpinned `npx` package execution introduces supply-chain risk by trusting the latest resolvable `tsx` package each time the script runs. In a security-sensitive skill related to payment authorization, even test scripts matter because developers may run them in trusted environments with credentials or local state.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding
The script performs another runtime execution of unversioned `tsx` through `npx`, allowing code from the package registry to influence local execution. This is particularly risky in payment-related tooling because it could tamper with preflight outcomes such as `ALLOW`, `DENY`, or `CONFIRM_REQUIRED`.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding
Using `npx -y tsx` without version pinning exposes the script to registry or dependency compromise, enabling arbitrary code execution under the user's permissions. Since this invocation records a payment event, an attacker who controls the executed tool could alter transaction logs or exfiltrate data.

Rp1

Medium
Category
MCP Rug Pull
Confidence
90% confidence
Finding
This final use of `npx -y tsx` has the same supply-chain weakness: the package version is not fixed, so script behavior can change or become malicious without repository changes. In the context of a payment guard skill, that increases risk because operational reporting and validation outputs may be manipulated.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/signet-cli.ts:644