Back to skill

Security audit

Project Memory Ledger

Security checks for vulnerabilities and agentic risk

Overview

This is a real project ledger tool, but it needs Review because its documented local-only behavior does not match the code and some project names can steer writes outside intended locations.

Install only if you are comfortable reviewing its code and constraining where it can write. Prefer backend=local, use a dedicated workspace path, avoid untrusted project names, and do not enable gws/Drive access until the local-only documentation mismatch, path containment, and Drive query escaping are fixed.

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
scripts/ledger.py:218
Finding

Path Traversal Allows File Creation Outside the Configured Project Root

Content
View full analysis
dict: root = Path(cfg.get('projects_root_dir') or './projects') project_dir = root / display_name sub = {} for name in ['Docs','Specs','Data','Backlog','Evidence','Releases','Archive']: p = project_dir / name p.mkdir(parents=True, exist_ok=True) sub[name] = p # starter markdown docs charter = sub['Docs'] / '项目说明(Charter).md' prd = sub['Docs'] / 'PRD(需求文档).md' backlog = sub['Backlog'] / 'Backlog(待办).md' ``` ### Technical Analysis The `display_name` parameter originates from the user-controlled `register-project --name` argument. It is directly joined to `projects_root_dir` without validation or containment verification. Python's `pathlib` path joining does not enforce confinement: - An absolute `display_name` replaces the configured root. - A relative name containing `..` components can escape the configured root. - Existing symbolic links in the selected path can also redirect file operations outside the expected project directory. The function subsequently creates multiple directories and writes or touches Markdown files at the resulting destination. Normalizing a separate `slug` value does not mitigate the issue because `display_name`, rather than `slug`, is used to construct the filesystem path. ### Attack Path 1. An attacker or untrusted automation gains control over arguments passed to `register-project`. 2. The attacker supplies a project name containing traversal components or an absolute path, such as `../../attacker-controlled`. 3. `scaffold_local_project()` evaluates `root / display_name` without rejecting the unsafe path. 4. Directory creation escapes `projects_root_dir`. 5. The function ...[truncated 812 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ledger.py:172
Finding

Google Drive Query Injection Through an Unescaped Project Name

Content
View full analysis
str: q = f"'{parent_id}' in parents and name='{name}' and mimeType='application/vnd.google-apps.folder' and trashed=false" params = json.dumps({"q": q, "pageSize": 5, "fields": "files(id,name)"}) out = run_gws(['drive','files','list','--params', params]) files = jx(out).get('files', []) if files: return files[0]['id'] body = json.dumps({"name": name, "mimeType": "application/vnd.google-apps.folder", "parents": [parent_id]}) out = run_gws(['drive','files','create','--json', body]) return jx(out)['id'] ``` ### Technical Analysis The folder `name` is interpolated directly into a Google Drive search query enclosed by single quotes. In the project-scaffolding flow, the initial value comes from the user-controlled `register-project --name` argument. A name containing a quote or Drive query operators can terminate the intended string literal and change the query's semantics. Although subprocess execution safely uses an argument list and does not invoke a shell, that does not prevent injection into the query language interpreted by Google Drive. The function also trusts the first returned result without independently verifying that: - Its name exactly matches the requested name. - Its parent is the intended parent folder. - It is the folder that the caller intended to reuse. Consequently, an altered query can cause an unrelated existing folder to be selected. ### Attack Path 1. Drive mode is enabled and the process uses an authenticated `gws` client. 2. An attacker or untrusted automation controls the `--name` argument passed to `register-project`. 3. The attacker supplies a project name containing a quote and crafted Drive query syntax. 4. `ensure_drive_folder()` inser ...[truncated 1030 chars]
Remediation
View remediation
str: return value.replace("\\", "\\\\").replace("'", "\\'") safe_parent = escape_drive_literal(parent_id) safe_name = escape_drive_literal(name) q = ( f"'{safe_parent}' in parents and " f"name='{safe_name}' and " "mimeType='application/vnd.google-apps.folder' and trashed=false" ) ``` After the query, the returned folder metadata should be checked for an exact name and intended parent match before its ID is accepted. ]]>
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (8)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The documentation overstates safety and capability by describing evidence, rollback, conversation-memory benefits, and backend behavior that the underlying implementation reportedly does not actually provide or only partially provides. In a memory or record-keeping skill, this can mislead operators into relying on nonexistent safeguards, causing bad state to be recorded, trusted, or left unrecoverable after mistakes.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

High
Category
Not specified by scanner
Confidence
96% confidence
Finding

The module docstring states that the ledger is always written to local Markdown and that drive mode only affects project-doc scaffolding. However, the append command explicitly writes invariants/decisions/changes to Drive documents when backend is set to drive, using cfg["docs"] IDs and append_drive(). This is an active contradiction between documented behavior and implemented behavior.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill advertises executable workflows involving local file writes, shell execution via python3, and optional networked Drive operations via gws, but it does not declare any explicit tool scope such as permissions or allowed-tools. That creates a trust and containment gap: an agent may invoke broader capabilities than a reviewer expects, increasing the chance of unintended filesystem modification or external data access.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/ledger.py (reported line 54)May include surrounding context.

python
def run(args: list[str]) -> str:
    """Run a command without using a shell (safer)."""
    p = subprocess.run(args, check=True, capture_output=True, text=True)
    return p.stdout.strip()

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The code sets config.language to 'en' whenever it is missing and emits a message instructing users to set language=zh for Chinese. This establishes English as the default locale choice rather than requiring an explicit user selection, which is a natural-language locale policy concern.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

update-prd claims to store patch proposals locally, but it calls append_local(cfg, 'prd_patches', proposal) even though local_path() does not define a prd_patches mapping. This causes a runtime failure, which can break audit logging and prevent preservation of change proposals that users may rely on for traceability and rollback.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The configuration section says backend supports only local | drive, but the very next line and later sections describe behavior for both (If backend is drive or both, drive/both, local/both). This is an active contradiction in the file's own usage/documentation and can mislead users about actual supported behavior.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
91% confidence
Finding

The configuration sets the skill language to "en" by default, which is a natural-language locale constraint. In this file there is no indication that users can choose a different language or that the English default is justified by a documented region-specific requirement.

Content

No source excerpt is available for this finding.