Back to skill

Security audit

Pocketbook

Security checks for vulnerabilities and agentic risk

Overview

Pocketbook is a coherent local personal-ledger skill, but it needs review because it stores sensitive finance data in ordinary files and includes an unscoped Markdown output path that can overwrite any writable file.

Review before installing. Use a private data directory, avoid passing --output except to the intended Pocketbook Markdown file, and consider disabling implicit invocation or requiring confirmation before writes. Configure timezone/currency if Asia/Shanghai and CNY are not your intended defaults.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ledger_common.py:73
Finding

Sensitive financial data is stored without explicit private file permissions

Content
View full analysis
Path: raw = data_dir or os.environ.get("POCKETBOOK_DATA_DIR") resolved = Path(raw).expanduser() if raw else DEFAULT_DATA_DIR resolved.mkdir(parents=True, exist_ok=True) return resolved ``` ```python def write_json_atomically(path: str | Path, payload: Any) -> None: resolved = Path(path) temp_path = resolved.with_name(resolved.name + ".tmp") with open(temp_path, "w", encoding="utf-8", newline="\n") as handle: json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True) handle.write("\n") handle.flush() os.fsync(handle.fileno()) os.replace(temp_path, resolved) ``` ```python def append_event_to_path(path: str | Path, event: dict[str, Any]) -> None: resolved = Path(path) line = json.dumps(event, ensure_ascii=False, sort_keys=True) + "\n" with open(resolved, "a", encoding="utf-8", newline="\n") as handle: handle.write(line) handle.flush() os.fsync(handle.fileno()) ``` ```python with open(output_path, "w", encoding="utf-8", newline="\n") as handle: handle.write(content) ``` ### Technical Analysis The data directory and files are created using ordinary `mkdir()` and `open()` operations without explicitly enforcing private modes such as `0700` for the directory and `0600` for files. Their effective permissions therefore depend on the process umask. Existing files also retain their previous permissions. The files contain sensitive personal-finance information, including transaction amounts, accounts, payment methods, merchants, notes, profile aliases, and original user utterances. On a multi-user system with a permissive umask or pr ...[truncated 1221 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/render_finance_md.py:75
Finding

Unrestricted Markdown output path permits overwrite of arbitrary writable files

Content
View full analysis
int: configure_standard_streams() parser = argparse.ArgumentParser(description="Render personal_finance.md from a Pocketbook ledger.") parser.add_argument("--data-dir", default=None, help="Ledger data root.") parser.add_argument("--output", default=None, help="Optional output path.") parser.add_argument("--timezone", default=DEFAULT_TIMEZONE) args = parser.parse_args() content = render_markdown(args.data_dir, args.timezone) output_path = args.output or markdown_path(args.data_dir) with open(output_path, "w", encoding="utf-8", newline="\n") as handle: handle.write(content) print(output_path) return 0 ``` ### Technical Analysis The `--output` argument accepts an unrestricted filesystem path. The renderer opens that path in write mode, truncating an existing file, without requiring the destination to remain inside the Pocketbook data directory. It also does not reject symlinks or sensitive configuration and startup-file locations. Several ledger-controlled strings are interpolated directly into the generated Markdown. Control character ...[truncated 1614 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (11)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The declared description centers on end-user bookkeeping operations: adding expenses/income/transfers, querying summaries, reviewing entries, and modifying or undoing recent transactions. The supplied code does none of that. It implements a standalone CLI utility for viewing and modifying profile configuration data, specifically default field values and alias mappings, using load_profile/save_profile. While this may support a broader ledger system, this chunk’s actual purpose is profile configuration management, not personal bookkeeping entry handling. Therefore the code chunk materially differs from the declared skill behavior.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill invokes local Python scripts that read environment state and perform file reads/writes, but it declares no explicit tool scope or allowed-tools boundary. This creates an authorization gap where the runtime may permit broader filesystem access than users or reviewers expect, increasing the chance of unintended data exposure or modification in a skill that handles persistent personal finance records.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The default prompt is broad enough to match common finance-related requests such as summaries, corrections, and quick transaction capture, which increases the chance the skill is invoked when the user did not explicitly intend to operate the ledger. Because this skill writes to local persistence, an over-broad trigger can cause unintended recording, modification, or disclosure of personal financial data.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

Enabling implicit invocation without tight activation constraints allows the skill to trigger automatically from ordinary conversation about money. In a skill that can persist, correct, complete, or undo ledger entries, this creates a meaningful risk of unauthorized or accidental state changes and exposure of sensitive personal finance history.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The schema states the timezone is usually Asia/Shanghai, which imposes a specific locale expectation in natural-language documentation. The file does not offer a user choice or explain that the skill is intentionally region-specific, so this conflicts with the language/locale policy criteria.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The suggested profile.json uses CNY, Asia/Shanghai, and Chinese-language aliases as the default profile structure. Without an explicit statement that this is an optional region-specific example or that users may choose their own locale, the documentation effectively prescribes a specific language/locale.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The file sets DEFAULT_TIMEZONE = "Asia/Shanghai" and DEFAULT_CURRENCY = "CNY" as global defaults, which imposes a specific locale on users unless they explicitly override it. Under the policy, forcing a specific language/locale without opt-in is a natural-language policy concern unless the regional constraint is clearly documented and justified.

Content

No source excerpt is available for this finding.

Tainted flow: 'temp_path' from os.environ.get (line 319, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/ledger_common.py (reported line 320)May include surrounding context.

python
def write_json_atomically(path: str | Path, payload: Any) -> None:
    resolved = Path(path)
    temp_path = resolved.with_name(resolved.name + ".tmp")
    with open(temp_path, "w", encoding="utf-8", newline="\n") as handle:
        json.dump(payload, handle, ensure_ascii=False, indent=2, sort_keys=True)
        handle.write("\n")
        handle.flush()

Tainted flow: 'resolved' from os.environ.get (line 75, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/ledger_common.py (reported line 429)May include surrounding context.

python
def append_event_to_path(path: str | Path, event: dict[str, Any]) -> None:
    resolved = Path(path)
    line = json.dumps(event, ensure_ascii=False, sort_keys=True) + "\n"
    with open(resolved, "a", encoding="utf-8", newline="\n") as handle:
        handle.write(line)
        handle.flush()
        os.fsync(handle.fileno())

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
89% confidence
Finding

The instruction says to default date or time to now in Asia/Shanghai whenever the user does not specify it. This is a natural-language locale policy constraint that forces a specific regional setting without indicating user choice or opt-in.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/ledger_common.py (reported line 222)May include surrounding context.

python
import sys

    for stream_name in ("stdout", "stderr"):
        stream = getattr(sys, stream_name, None)
        if stream is not None and hasattr(stream, "reconfigure"):
            stream.reconfigure(encoding="utf-8")

Static analysis

No suspicious patterns detected.