Back to skill

Security audit

Passwordstore Broker

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent password-helper skill, but its LAN credential intake and TOTP enrollment handling create real secret-exposure risks users should review before installing.

Install only if you are comfortable with a local password broker handling real credentials. Prefer localhost mode, avoid the LAN phone flow on untrusted networks, treat the QR code and otpauth URL as secrets, and review every command that will run with injected credentials. Be aware that this stores account secrets in pass and stores the broker's TOTP seed locally under ~/.passwordstore-broker.

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

Error
Location
scripts/get_password_from_user.py:553
Finding
LAN Secret Intake Uses an Unverifiable Self-Signed TLS Certificate<![CDATA[ ## Vulnerability Details **File Location**: `scripts/get_password_from_user.py:553-585, 657-659` **Vulnerability Type**: Unauthenticated TLS endpoint for sensitive credential collection **Risk Level**: High ### Vulnerable Code ```python def ensure_self_signed_cert(hostname: str, cert_dir: str): cert_path = os.path.join(cert_dir, "cert.pem") key_path = os.path.join(cert_dir, "key.pem") openssl = shutil.which("openssl") if openssl is None: raise RuntimeError("openssl is required to generate a self-signed cert") san = "DNS:localhost,IP:127.0.0.1" try: ipaddress.ip_address(hostname) san = f"IP:{hostname},DNS:localhost,IP:127.0.0.1" except ValueError: san = f"DNS:{hostname},DNS:localhost,IP:127.0.0.1" subprocess.run( [ openssl, "req", "-x509", "-newkey", "rsa:2048", "-sha256", "-days", "1", "-nodes", "-keyout", key_path, "-out", cert_path, "-subj", f"/CN={hostname}", "-addext", f"subjectAltName={san}", ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) return cert_path, key_path ``` The generated certificate is then used directly by the intake server: ```python context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(certfile=cert_path, keyfile=key_path) httpd.socket = context.wrap_socket(httpd.socket, server_side=True) ``` ### Technical Analysis The LAN-mode intake page collects both an account secret and a current TOTP code. A new self-signed certificate is generated for every execution, but the implementation provides no trusted certificate authority, previously trusted public key, pinned fingerprint, or authenticated out-of-band verification procedure. TLS encryption without authenticated server ...[truncated 1947 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Make localhost-only intake the preferred and safest operating mode. 2. For LAN mode, use a certificate whose identity can be verified: - Issue the server certificate from a locally trusted private CA; or - Provision a persistent certificate and pin its public-key fingerprint in a previously authenticated setup step; or - Use an authenticated secure transport that already establishes server identity. 3. Communicate any certificate fingerprint through a channel independent of the LAN connection and require the user to verify it before entering credentials. 4. Do not instruct users to bypass an unverifiable browser certificate warning. 5. Consider requiring a challenge bound to the specific intake session in addition to TOTP. This complements, but does not replace, authenticated TLS. 6. Document the LAN threat model and explicitly state that subnet filtering and TOTP do not prevent man-in-the-middle interception. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/get_password_from_user.py:481
Finding
Unbounded Request Body and Threaded Handling Allow LAN Denial of Service<![CDATA[ ## Vulnerability Details **File Location**: `scripts/get_password_from_user.py:481-482, 647-652` **Vulnerability Type**: Unbounded HTTP request-body processing and connection resource exhaustion **Risk Level**: Medium ### Vulnerable Code ```python length = int(self.headers.get("Content-Length", "0")) raw = self.rfile.read(length).decode("utf-8", errors="replace") form = urllib.parse.parse_qs(raw) ``` The server also creates an unrestricted thread-per-connection service: ```python class ThreadingTCPServer(socketserver.ThreadingTCPServer): allow_reuse_address = True try: with ThreadingTCPServer((access.bind_host, args.port), OneShotHandler) as httpd: httpd.state = state # type: ignore[attr-defined] ``` ### Technical Analysis The handler converts the client-supplied `Content-Length` header to an integer and reads that amount without enforcing a maximum request size. A client can therefore advertise and transmit a very large body, causing excessive memory allocation during the read, UTF-8 decoding, and form parsing. A client can also advertise a large length and transmit the body very slowly or incompletely. Because no per-connection read timeout is configured, a handler thread may remain blocked waiting for data. The use of `ThreadingTCPServer` permits multiple such requests to consume threads and socket resources concurrently. LAN mode permits requests from every address in the automatically detected subnet, so possession of the one-time form token is not required to begin consuming these resources. An invalid or extremely large numeric header can also trigger unhandled conversion or processing behavior instead of a controlled HTTP rejection. ### Attack Path 1. The broker starts in LAN mode and binds to its private IPv4 address. 2. An attacker on the allowed subnet discovers or is informed of the listening address and port. 3. The attacker opens multiple TLS connections to the broker. 4. On each connection, the attacker send ...[truncated 891 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define a small maximum request size suitable for the expected secret and TOTP fields, such as 16 KiB. 2. Validate `Content-Length` before reading: - Reject missing or malformed values when a body is required. - Reject negative values. - Return HTTP `413 Payload Too Large` when the configured maximum is exceeded. 3. Read at most the configured maximum and reject excess input rather than buffering an arbitrary body. 4. Configure socket timeouts for TLS negotiation, headers, and body reads to mitigate slow-client attacks. 5. Limit concurrent handler threads or use a bounded worker pool. 6. Set `daemon_threads = True` and ensure stalled clients cannot prevent clean server shutdown. 7. Apply source-level rate limiting before expensive request processing. 8. Preserve token and TOTP checks, but do not rely on them for denial-of-service prevention because the body is currently consumed before authentication completes. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (25)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill claims secrets are collected through one-time HTTPS forms and used via an environment-injection wrapper so raw values do not enter chat context or logs, but the documented interface also includes direct secret retrieval via scripts/vault.sh get and the analysis indicates the promised collection/execution mechanisms may be absent. This is dangerous because it creates a false sense of safety: an agent may rely on protections that do not actually exist, causing direct secret disclosure to stdout, chat, or logs during normal operation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill claims secrets are collected through one-time HTTPS forms and used via an environment-injection wrapper so raw values do not enter chat context or logs, but the documented interface also includes direct secret retrieval via scripts/vault.sh get and the analysis indicates the promised collection/execution mechanisms may be absent. This is dangerous because it creates a false sense of safety: an agent may rely on protections that do not actually exist, causing direct secret disclosure to stdout, chat, or logs during normal operation.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
scripts/vault.sh put test/secret <<< "ok"
scripts/vault.sh exists test/secret
scripts/run_with_secret.sh --secret test/secret --env TEST_SECRET -- sh -c 'test "$TEST_SECRET" = "ok"'
scripts/vault.sh rm test/secret
```
Confidence
85% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The payload printed to stdout always includes the full otpauth URL, which embeds the raw TOTP shared secret, and error paths also print it to stderr. In a skill explicitly intended to keep raw secrets out of chat context and logs, emitting the enrollment secret directly defeats that guarantee and can allow anyone with log or transcript access to clone the second factor.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill declares operational steps that invoke local scripts, read and write files under the user's home directory, use networked HTTPS intake flows, and execute shell commands, yet it does not define any explicit tool scope or allowed-tools boundaries. That omission increases the chance an agent will run powerful filesystem, shell, and network actions without least-privilege controls or clear user-visible constraints.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
## Non-Negotiable Guardrails

- Never ask users to paste raw secrets into chat.
- Never echo secret values back to user.
- Never store secrets in repo files, commit messages, issue comments, or transcripts.
- Never expose intake over public interfaces or tunnels.
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
### Debian/Ubuntu

```bash
sudo apt-get update
sudo apt-get install -y pass gnupg2 openssl python3 qrencode
```
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
### Debian/Ubuntu

```bash
sudo apt-get update
sudo apt-get install -y pass gnupg2 openssl python3 qrencode
```
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
### Debian/Ubuntu

```bash
sudo apt-get update
sudo apt-get install -y pass gnupg2 openssl python3 qrencode
```
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
### Debian/Ubuntu

```bash
sudo apt-get update
sudo apt-get install -y pass gnupg2 openssl python3 qrencode
```
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Session Persistence

Medium
Category
Rogue Agent
Content
## Initialize password store

1. Create a GPG key (if needed):

```bash
gpg --full-generate-key
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
raise RuntimeError("Missing required command 'ip' for Linux LAN detection")

    try:
        route_proc = subprocess.run(
            [ip_cmd, "-j", "route", "get", "1.1.1.1"],
            check=True,
            stdout=subprocess.PIPE,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
raise RuntimeError("Linux LAN detection failed: route output missing interface name")

    try:
        addr_proc = subprocess.run(
            [ip_cmd, "-j", "-f", "inet", "addr", "show", "dev", iface_name],
            check=True,
            stdout=subprocess.PIPE,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
raise RuntimeError("Missing required command 'route' for macOS LAN detection")

    try:
        route_proc = subprocess.run(
            [route_cmd, "-n", "get", "default"],
            check=True,
            stdout=subprocess.PIPE,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
raise RuntimeError("macOS LAN detection failed: default route missing interface name")

    try:
        proc = subprocess.run(
            ["ifconfig", iface_name],
            check=True,
            stdout=subprocess.PIPE,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return

        try:
            subprocess.run(
                [self.state.vault_script, "put", self.state.secret_name],
                input=secret_value.encode("utf-8"),
                check=True,
Confidence
79% confidence
Finding
This code accepts a remotely submitted secret over the HTTPS form and forwards it directly to an external script, vault.sh, using a secret name supplied from the command line. While the subprocess call itself is not shell-injectable, the security boundary is weak because untrusted input is handed to another executable with no local validation of the secret identifier or confinement of what vault.sh may write, so a malformed or unexpected secret name could become dangerous depending on vault.sh's implementation. In this skill context, which is explicitly designed to handle secrets, this trust boundary matters more because it can lead to secret misrouting, overwrite of unintended entries, or downstream injection in the storage helper.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
except ValueError:
        san = f"DNS:{hostname},DNS:localhost,IP:127.0.0.1"

    subprocess.run(
        [
            openssl,
            "req",
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
if sys.platform == "darwin":
        return "brew install qrencode"
    if shutil.which("apt-get"):
        return "sudo apt-get install -y qrencode"
    if shutil.which("dnf"):
        return "sudo dnf install -y qrencode"
    if shutil.which("pacman"):
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
if sys.platform == "darwin":
        return "brew install qrencode"
    if shutil.which("apt-get"):
        return "sudo apt-get install -y qrencode"
    if shutil.which("dnf"):
        return "sudo dnf install -y qrencode"
    if shutil.which("pacman"):
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
if sys.platform == "darwin":
        return "brew install qrencode"
    if shutil.which("apt-get"):
        return "sudo apt-get install -y qrencode"
    if shutil.which("dnf"):
        return "sudo dnf install -y qrencode"
    if shutil.which("pacman"):
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
This script introduces a new secret-management path for TOTP enrollment that is outside the skill's stated design of handling secrets via one-time HTTPS forms and pass-backed storage. In a secret-handling skill, undocumented alternate secret flows increase the attack surface, bypass expected controls, and can lead operators to assume protections exist where they do not.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The script stores the TOTP shared secret directly in ~/.passwordstore-broker/totp.secret instead of using the manifest-described pass storage path. Even with chmod 0600, this creates an unmanaged plaintext secret at rest that may be copied by backups, endpoint tooling, or local compromise, undermining the broker's stated safe-secret-handling model.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
When qrencode is missing, the script prints a manual enrollment URL containing the full TOTP secret to stderr without warning. stderr is frequently captured by terminals, CI systems, wrappers, or agent logs, so this can leak the shared secret and permit unauthorized enrollment of a duplicate authenticator.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return 2

    try:
        subprocess.run(
            [qrencode, "-o", str(qr_png_path), "-t", "PNG", otpauth_url],
            check=True,
            stdout=subprocess.DEVNULL,
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
On QR generation failure, the script again prints the full otpauth enrollment URL, exposing the TOTP shared secret during an error path where users are likely to paste logs for troubleshooting. Error-path secret disclosure is especially risky because it propagates secrets into support channels and automation logs outside the intended secret boundary.