Back to skill

Security audit

Otpforge

Security checks for vulnerabilities and agentic risk

Overview

OtpForge appears to be a legitimate local TOTP manager, but it handles long-lived 2FA secrets in ways users should review carefully before installing.

Review this skill before installing if you would store real account MFA seeds. Avoid entering TOTP seeds on the command line, do not place the vault in shared or synced folders, restrict access to the vault file, and prefer an encrypted password manager or OS-backed authenticator for important accounts.

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
cli.py:22
Finding
TOTP Secret Exposure Through Command-Line Arguments<![CDATA[ ## Vulnerability Details **File Location**: `cli.py:22-24` (the insecure usage is also documented in `SKILL.md:15-16`) **Vulnerability Type**: Sensitive information exposure through process arguments and shell history **Risk Level**: High ### Vulnerable Code ```python add = sub.add_parser("add", help="Add or update an account") add.add_argument("label", help="Account label, e.g. email@domain.com") add.add_argument("secret", help="Base32 secret key") ``` The documented invocation reinforces this insecure interface: ```text - Add/update an account: - `python3 cli.py add <label> <base32-secret> --issuer <issuer> --digits 6 --period 30` ``` ### Technical Analysis The application accepts a TOTP seed as a positional command-line argument. Command-line arguments are not an appropriate channel for authentication secrets because they may be exposed through: - Shell history files. - Process inspection utilities while the command is running. - Operating-system process accounting or auditing. - Terminal session recording and command telemetry. - Diagnostic logs that capture executed command lines. A TOTP seed is a long-lived credential rather than a single-use code. Anyone who obtains it can generate future valid codes for the associated account, subject only to the configured time period and clock synchronization. ### Attack Path 1. A user follows the documented command and runs `python3 cli.py add account BASE32_SECRET`. 2. The shell records the complete command in its history, or a local process-monitoring facility captures the process arguments. 3. An attacker with access to that history, telemetry, or process information extracts the Base32 seed. 4. The attacker imports the seed into another TOTP implementation. 5. The attacker generates valid current and future TOTP codes and uses them with separately obtained account credentials. ### Impact Assessment Successful exploitation discloses the second-factor seed for the account being added. The a ...[truncated 316 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the TOTP secret positional argument from the CLI. - Prompt for the secret using `getpass.getpass()` so input is neither echoed nor included in process arguments. - For automation, accept the secret through a protected file descriptor or standard input, while clearly warning users not to place it directly in shell command text. - If file-based import is supported, verify restrictive ownership and permissions before reading the secret. - Update `SKILL.md` to document the protected input workflow and remove examples containing a secret argument. - Warn existing users to clear affected shell history securely and rotate any TOTP seeds previously entered on the command line. - Add automated tests confirming that parsed command-line arguments no longer contain the secret. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
core.py:86
Finding
TOTP Seeds Stored Unencrypted in the Local JSON Vault<![CDATA[ ## Vulnerability Details **File Location**: `core.py:86-94` and `core.py:125-133` **Vulnerability Type**: Plaintext storage of sensitive authentication data **Risk Level**: Medium ### Vulnerable Code The secret is inserted directly into the JSON data structure: ```python items.append( { "label": normalized.label, "issuer": normalized.issuer, "secret": normalized.secret, "digits": normalized.digits, "period": normalized.period, } ) self._write_raw(data) ``` The complete data structure is then serialized as plaintext: ```python def _write_raw(self, data: dict) -> None: self._ensure_parent() temp = self.path.with_suffix(".tmp") with temp.open("w", encoding="utf-8") as fh: json.dump(data, fh, indent=2) fh.write("\n") os.replace(temp, self.path) os.chmod(self.path, 0o600) ``` ### Technical Analysis The application writes long-lived TOTP seeds directly to a human-readable JSON file. Applying mode `0600` after replacement is a useful access-control measure, but it is not encryption and does not protect the secrets from: - Malware or an attacker operating under the same user account. - Accidental copies, archives, or backups of the vault. - Offline disk or filesystem acquisition. - Privileged local users. - Other processes that read the file before permissions are corrected after replacement. The temporary file is created using the process's current `umask`, and restrictive permissions are applied only to the destination after `os.replace`. Consequently, the temporary plaintext file may briefly have broader permissions than intended under a permissive `umask`. ### Attack Path 1. The user adds one or more TOTP accounts. 2. `_write_raw` serializes every seed into a plaintext temporary JSON file and replaces the configured vault file with it. 3. An attacker obtains user-context file access, privileged local access, access to a backup, or access to the temporary file w ...[truncated 695 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Store TOTP seeds in an operating-system credential store where available. - If a portable vault is required, encrypt it using authenticated encryption such as AES-GCM or ChaCha20-Poly1305. - Derive encryption keys from a user passphrase using a memory-hard KDF such as Argon2id or scrypt, with a unique random salt and appropriate cost parameters. - Prefer a key protected by the platform keychain rather than storing the encryption key beside the vault. - Authenticate all encrypted metadata to prevent undetected modification of labels, digit counts, periods, and ciphertext. - Create temporary files atomically with mode `0600` from the outset instead of relying on a later `chmod`. - Preserve restrictive directory permissions and verify vault ownership before reading or writing. - Avoid plaintext backups and securely migrate or delete existing plaintext vaults after encryption is enabled. - Require users to rotate seeds if an existing plaintext vault may have been exposed. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (7)

Credential Access

High
Category
Privilege Escalation
Content
if override:
        return Path(override).expanduser().resolve()
    home = Path.home()
    return home / ".config" / APP_NAME / "secrets.json"


def _normalize_secret(secret: str) -> str:
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
if override:
        return Path(override).expanduser().resolve()
    home = Path.home()
    return home / ".config" / APP_NAME / "secrets.json"


def _normalize_secret(secret: str) -> str:
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
if override:
        return Path(override).expanduser().resolve()
    home = Path.home()
    return home / ".config" / APP_NAME / "secrets.json"


def _normalize_secret(secret: str) -> str:
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
if override:
        return Path(override).expanduser().resolve()
    home = Path.home()
    return home / ".config" / APP_NAME / "secrets.json"


def _normalize_secret(secret: str) -> str:
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill advertises capabilities that clearly involve reading environment variables, reading and writing local files, and invoking Python from the shell, but it does not declare any tool scope or permission boundaries. This weakens least-privilege controls and makes it easier for an agent runtime to grant broader access than users expect, especially because the skill handles highly sensitive TOTP secrets.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The skill description and usage notes instruct users to store TOTP seeds in a local JSON vault but do not warn that these secrets are equivalent to second-factor credentials and require strong local protection. Users may unknowingly place reusable MFA secrets in plaintext or weakly protected storage, increasing the chance of account compromise if the host or file is exposed.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The vault writes TOTP secrets directly into a JSON file in plaintext, which means any local user, malware, backup system, or process with filesystem access can recover the underlying 2FA seeds and generate valid codes indefinitely. Although the file permissions are tightened to 0600 after writing, the data is still unencrypted at rest and the temporary file may briefly exist before replacement, so the skill context of storing authentication factors locally makes this especially sensitive.

Static analysis

No suspicious patterns detected.