Back to skill

Security audit

database_skill

Security checks for vulnerabilities and agentic risk

Overview

The skill does provide database access as advertised, but it handles saved connection data, credentials, and database-changing commands too loosely for a clean install.

Review before installing. Use only least-privilege database accounts, avoid production mutation unless explicitly intended, avoid --batch for untrusted SQL, do not pass literal passwords or shell-expanded secrets on the command line, do not embed credentials in database URLs, and avoid this on shared machines unless the connection store is moved to a private protected location and TLS is enforced.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/main.py:45
Finding
Database Password Exposure Through Command-Line Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/main.py:45`, `scripts/main.py:171-187`; documented usage at `SKILL.md:80-86` **Vulnerability Type**: Plaintext sensitive data exposure through process arguments and shell history **Risk Level**: High ### Vulnerable Code ```python parser.add_argument("--password", help="Database password.") ``` ```python if args.url: url = args.url username = args.user or "" raw_password = args.password or "" # Detect "${VAR_NAME}", "${env:VAR_NAME}", "$env:VAR_NAME", or plain "VAR_NAME" m = _re.match(r"^\$\{(?:env:)?(\w+)\}$", raw_password) if m: password_env_var = m.group(1) password = os.environ.get(password_env_var, "") elif _re.match(r"^\$env:(\w+)$", raw_password, _re.IGNORECASE): password_env_var = _re.match(r"^\$env:(\w+)$", raw_password, _re.IGNORECASE).group(1) password = os.environ.get(password_env_var, "") elif _re.match(r"^[A-Z_][A-Z0-9_]*$", raw_password.upper()) and not _re.match(r"^\d", raw_password): password_env_var = raw_password password = os.environ.get(password_env_var, "") elif raw_password: password = raw_password ``` The documented configuration method also explicitly recommends the command-line option: ```bash python scripts/main.py \ --url "jdbc:mysql://host:3306/db" \ --user "admin" \ --password "${DB_PASS}" \ --query "SELECT 1" ``` ### Technical Analysis The CLI accepts a database password directly in `argv`. When a literal password is supplied, it can be retained in shell history and may be visible through process-inspection facilities while the command is running. Although shell expansion of `"${DB_PASS}"` avoids placing the variable name in the resulting command, it substitutes the secret before process creation. The expanded password therefore still becomes a process argument. This exposure is not necessary for the Skill's database functionality. Passwords can instead be obtaine ...[truncated 1133 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove or strongly discourage literal passwords in `--password`. - Add an option such as `--password-env DB_PASS` that accepts only an environment-variable name. - For interactive use, obtain the password with `getpass.getpass()` rather than `input()` or a command-line argument. - Support protected secret files or inherited file descriptors for automated deployments. - If backward compatibility requires `--password`, emit a prominent warning when the argument contains a literal value. - Update all documentation to avoid examples that expand secrets directly into command-line arguments. - Clear password references as soon as practical after the connection has been established. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/connections_store.py:142
Finding
Connection Metadata Stored in a Predictable Temporary File Without Explicit Access Controls<![CDATA[ ## Vulnerability Details **File Location**: `scripts/connections_store.py:11`, `scripts/connections_store.py:142-151` **Vulnerability Type**: Unsafe temporary-file storage and missing file permission enforcement **Risk Level**: High ### Vulnerable Code ```python _STORE_FILE = os.path.join(tempfile.gettempdir(), ".database-skill-connections.json") ``` ```python def clear(self) -> None: self._save_all([]) def _save_all(self, records: List[ConnectionRecord]) -> None: try: with open(self._path, "w") as f: json.dump([r.to_dict() for r in records], f, indent=2) except Exception as exc: logger.debug("Failed to save connections: %s", exc) ``` The records written to that file include connection metadata: ```python def to_dict(self) -> Dict[str, str]: d: Dict[str, str] = { "url": self.url, "username": self.username, "driver": self.driver, "label": self.label, } if self.password_env_var: d["password_env_var"] = self.password_env_var return d ``` ### Technical Analysis The store uses a fixed filename in the system temporary directory. On multi-user systems, this directory is commonly shared. The code writes with ordinary `open(..., "w")` and does not: - Create the file with owner-only permissions. - Verify that the destination is a regular file owned by the current user. - Reject symbolic links. - Use exclusive or atomic file creation. - Separate records belonging to different operating-system users. The resulting permissions depend on the process umask. A typical permissive umask can create a file readable by other local users. If an attacker can pre-create the predictable path as a symbolic link, the write may follow that link and truncate or replace another file that the victim process is authorized to write. No database password value is intentionally placed in this record, but the URL, username, driver, label, and secret environment-variable name are sen ...[truncated 1602 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Store records in a per-user configuration or state directory rather than the shared temporary directory. - Create the parent directory with owner-only permissions such as `0700`. - Create new files with mode `0600`, independent of the ambient umask. - Refuse to open symbolic links and verify the destination is a regular file owned by the current user. - Use an atomic write pattern: create a protected temporary file in the same private directory, flush and synchronize it, then replace the destination atomically. - Do not persist URLs containing user-info credentials, access tokens, or sensitive query parameters. - Maintain separate stores per operating-system user. - Treat failures to enforce ownership or permissions as visible security errors rather than silently logging them at debug level. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/main.py:132
Finding
Unredacted Database URLs Are Persisted and Printed<![CDATA[ ## Vulnerability Details **File Location**: `scripts/main.py:132-143`, `scripts/main.py:252-260`; serialization at `scripts/connections_store.py:48-58` **Vulnerability Type**: Sensitive information exposure through persistent storage and terminal output **Risk Level**: High ### Vulnerable Code ```python if args.list_connections: records = _store.load_all() if not records: print("No saved connections.") return print("Saved connections:") for i, r in enumerate(records): pwd_info = f"env:${r.password_env_var}" if r.has_password() else "manual input" print(f" [{i}] {r.label}") print(f" URL: {r.url}") print(f" User: {r.username}") print(f" Type: {r.driver}") print(f" Password: {pwd_info}") return ``` ```python # Save connection on success record = ConnectionRecord( url=url, username=username, driver=driver, password_env_var=password_env_var, ) _store.save(record) ``` ```python def to_dict(self) -> Dict[str, str]: d: Dict[str, str] = { "url": self.url, "username": self.username, "driver": self.driver, "label": self.label, } if self.password_env_var: d["password_env_var"] = self.password_env_var return d ``` ### Technical Analysis The Skill accepts a caller-controlled JDBC URL, preserves the complete string, saves it after successful operations, and later prints it without redaction. The implementation does not reject URL user-info or remove sensitive query parameters. Consequently, a URL containing embedded credentials, tokens, private connection options, or internal host information is persisted even though the documentation states that passwords are never stored. The name of the password-bearing environment variable is also displayed. While this is not the password value, it gives a local attacker useful information for targeting process environments, deployment configuration, ...[truncated 977 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse and validate every JDBC URL before use. - Reject URLs containing passwords, tokens, or other credentials in user-info or query parameters. - Persist a normalized connection identifier containing only the scheme, host, port, and database name. - Maintain a denylist or allowlist for sensitive query-parameter names and redact them before storage or display. - Redact usernames and internal host details where full output is not required. - Do not print secret environment-variable names by default. - Add regression tests covering URLs with user-info, passwords, tokens, and sensitive query parameters. - Correct the documentation so its “passwords are never stored” guarantee matches enforced behavior rather than caller convention. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/connection_manager.py:177
Finding
Database Authentication and Query Traffic Are Not Protected by Enforced TLS<![CDATA[ ## Vulnerability Details **File Location**: `scripts/connection_manager.py:177-193`; insecure defaults at `config/datasource.yml:2`, `config/datasource-test.yml:2`, and `references/supported-databases.md:12` **Vulnerability Type**: Missing transport encryption and certificate verification **Risk Level**: High ### Vulnerable Code ```python if self._driver == "pymysql": pymysql = _import_pymysql() parsed = urllib.parse.urlparse(python_url) host = parsed.hostname or "localhost" port = parsed.port or 3306 database = parsed.path.lstrip("/").split("?")[0] if parsed.path else "" try: return pymysql.connect( host=host, port=port, database=database, user=self._username, password=self._password, cursorclass=pymysql.cursors.DictCursor, charset="utf8mb4", ) ``` The distributed configuration explicitly disables SSL: ```yaml datasource: url: "jdbc:mysql://localhost:3306/your_database?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC" ``` The test configuration has the same behavior: ```yaml datasource: url: "jdbc:mysql://localhost:3306/orange?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC" ``` The reference documentation also recommends an insecure URL: ```text jdbc:mysql://<host>:3306/<db>?useSSL=false&charset=utf8mb4 ``` ### Technical Analysis Sending database credentials to the user-selected database server is necessary for the declared connectivity functionality and is not, by itself, evidence of malicious exfiltration. However, the connector does not configure a TLS context, CA trust, certificate verification, or hostname verification. The JDBC query string is parsed only to obtain the hostname, port, and database path. Its TLS-related options are not translated into PyMySQL connection options. Therefore, the insecure `useSSL=false` examples reinforce plaintext transport, while changing that query pa ...[truncated 1528 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Enable TLS by default for every network database driver. - For PyMySQL, construct an SSL context that requires certificate validation and hostname verification, then pass it through the driver's supported SSL option. - Provide explicit CA-file, client-certificate, and client-key configuration where mutual TLS is required. - Remove every `useSSL=false` example from configuration and documentation. - Reject remote non-TLS connections by default; require an explicit, prominently warned opt-out only for trusted local development. - Translate supported connection options into native Python driver parameters rather than silently ignoring the JDBC query string. - Apply equivalent verified-TLS controls to PostgreSQL, Oracle, and SQL Server drivers. - Add tests confirming that remote connections fail when certificates are invalid, untrusted, expired, or issued for another hostname. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • 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
Findings (10)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
Persisting database connection records to a temp-directory JSON file creates a real security concern even if passwords are omitted, because URLs, usernames, hostnames, and driver metadata are sensitive operational information. In multi-user or weakly isolated environments, temp directories may expose connection inventories that aid lateral movement, credential targeting, or reconnaissance against internal databases.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
Persisting database connection records to a temp-directory JSON file creates a real security concern even if passwords are omitted, because URLs, usernames, hostnames, and driver metadata are sensitive operational information. In multi-user or weakly isolated environments, temp directories may expose connection inventories that aid lateral movement, credential targeting, or reconnaissance against internal databases.

Tp4

High
Category
MCP Tool Poisoning
Confidence
89% confidence
Finding
Persisting database connection records to a temp-directory JSON file creates a real security concern even if passwords are omitted, because URLs, usernames, hostnames, and driver metadata are sensitive operational information. In multi-user or weakly isolated environments, temp directories may expose connection inventories that aid lateral movement, credential targeting, or reconnaissance against internal databases.

Intent-Code Divergence

High
Confidence
97% confidence
Finding
The code claims plain-text passwords are never persisted, but `to_dict()` always serializes the full `url`, and many database URL formats allow embedded credentials such as `user:password@host` or password query parameters. This can silently store secrets on disk in a predictable temp-directory file, creating credential exposure to other local users, backup systems, or forensic recovery.

Memory Manipulation

High
Category
Memory Poisoning
Content
self._cm.close_connection(conn)

    def execute_update(self, sql: str, *params: Any) -> int:
        """Execute an UPDATE, INSERT, or DELETE statement.

        Args:
            sql: SQL statement with ``?`` placeholders.
Confidence
80% confidence
Finding
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill declares significant capabilities—environment-variable access, file read/write, and network/database connectivity—but does not define any explicit tool scope or permission boundaries. In an agent setting, this increases the chance of overbroad execution, misuse of credentials, unintended data exfiltration, or destructive database operations because the agent may invoke capabilities beyond what users expect.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This code expands ${VAR} placeholders by reading from process environment variables, which can expose or consume sensitive credentials without any confirmation prompt, user-facing log, or warning comment/docstring about that behavior. For code files, access to sensitive environment variables should have some visible disclosure unless clearly documented elsewhere.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
Connection metadata is written to a JSON file in the system temp directory without any explicit disclosure or consent flow. In this skill context, saved records may include sensitive infrastructure details such as internal hosts, database names, usernames, labels, and potentially credentials via the URL, making temp-directory persistence materially riskier than ordinary application state.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The CLI explicitly supports UPDATE/INSERT/DELETE operations, and later executes them directly via `execute_update` without any confirmation prompt or upfront warning about modifying database state. Because these are potentially destructive or irreversible operations, the file should provide some visible disclosure beyond the terse argument help.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The `--batch` option accepts a file of SQL statements and `_do_batch` executes them all, which can include destructive changes, but the code provides no confirmation prompt or explicit warning about the risk. This is a safety-critical operation because it can modify or destroy large amounts of data in one run.

Static analysis

Detected: suspicious.exposed_secret_literal

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/main.py:218