Back to skill

Security audit

Trifle Auth

Security checks for vulnerabilities and agentic risk

Overview

This skill performs the advertised Trifle wallet login, but it handles wallet keys and long-lived tokens with under-scoped network and local-storage protections.

Review before installing. Use a dedicated low-value wallet, do not set TRIFLE_BACKEND_URL except to a trusted HTTPS Trifle endpoint, verify the 1Password path before login, and restrict the JWT state file permissions. Treat the post-install npm install as code execution from dependencies, and avoid relying on the token temp-file command until it is made race-resistant.

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
trifle-auth.mjs:39
Finding
Configurable backend permits JWT and SIWE credential disclosure to an arbitrary server## Vulnerability Details **File Location**: `trifle-auth.mjs`, lines 39-41 and 96-134 **Vulnerability Type**: Unrestricted credential destination and cleartext transport permitted **Risk Level**: High ```javascript const STATE_FILE = process.env.TRIFLE_AUTH_STATE || join(STATE_DIR, 'auth-state.json'); const SETTINGS_FILE = join(CONFIG_DIR, 'settings.json'); const BACKEND_URL = process.env.TRIFLE_BACKEND_URL || SERVERS.live; async function apiRequest(path, options = {}) { const url = `${BACKEND_URL}${path}`; const res = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', 'Origin': 'https://trifle.life', 'Referer': 'https://trifle.life/', ...options.headers, }, }); if (!res.ok) { const text = await res.text(); throw new Error(`API error ${res.status}: ${text}`); } return res.json(); } async function authenticatedRequest(path, options = {}) { const state = loadState(); if (!state.token) { throw new Error('Not authenticated. Run: trifle-auth.mjs login'); } return apiRequest(path, { ...options, headers: { 'Authorization': `Bearer ${state.token}`, ...options.headers, }, }); } ``` During login, the same unrestricted request function also transmits the signed SIWE message: ```javascript const result = await apiRequest('/auth/wallet/verify', { method: 'POST', body: JSON.stringify({ signature, message, chainId: mainnet.id, }), }); ``` ### Technical Analysis `TRIFLE_BACKEND_URL` completely controls the origin receiving authentication traffic. The value is not parsed, restricted to an approved hostname, or required to use HTTPS. Consequently, authenticated commands send the stored bearer JWT to any configured destination, including an attacker-controlled server or a cleartext HTTP endpoint. Sending a SIWE signature and JWT to ...[truncated 1743 chars]
Remediation
## Remediation Suggestions - Remove `TRIFLE_BACKEND_URL` unless arbitrary backends are a documented and essential feature. - If staging support is required, accept only symbolic values such as `live` and `staging`, then map them internally to the hardcoded HTTPS URLs. - Parse the final URL with `new URL()` and require: - `protocol === 'https:'` - an exact allowlisted hostname - no embedded username or password - an expected port - Refuse redirects for requests carrying JWTs, or validate every redirect destination before forwarding credentials. - Maintain separate authentication state per approved server so a staging token cannot be sent to production or vice versa. - Never attach `Authorization` automatically to a request until the final destination has passed origin validation. - Add automated tests proving that HTTP URLs, lookalike domains, user-info URL tricks, unexpected ports, and redirects to unapproved hosts are rejected.

T09 · Insecure Skill Coding Practices

Error
Location
trifle-auth.mjs:53
Finding
JWT state is created without restrictive directory or file permissions## Vulnerability Details **File Location**: `trifle-auth.mjs`, lines 53-67 **Vulnerability Type**: Insecure storage permissions for a long-lived bearer token **Risk Level**: High ```javascript // Ensure state and config directories exist mkdirSync(STATE_DIR, { recursive: true }); mkdirSync(CONFIG_DIR, { recursive: true }); function loadState() { try { return JSON.parse(readFileSync(STATE_FILE, 'utf8')); } catch { return { token: null, address: null, userId: null, username: null, lastLogin: null }; } } function saveState(state) { writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); } ``` The saved state includes the bearer token: ```javascript const state = { token: result.token, address: account.address, userId: userInfo.id || null, username: userInfo.username || null, totalBalls: userInfo.totalBalls || null, lastLogin: new Date().toISOString(), }; saveState(state); ``` ### Technical Analysis Neither state directory creation nor state-file creation specifies a restrictive mode. Permission selection is therefore delegated to the process umask. With a common umask of `022`, directories are typically created as `0755` and the JSON state file as `0644`, allowing other local users to traverse the directory and read the JWT. The implementation also fails to correct permissions on an existing state file. A token file created under a permissive umask remains exposed after subsequent logins. The private-key fallback and temporary token paths explicitly request mode `0600`, demonstrating that sensitive-file permissions are an intended security property. The primary JWT state does not receive equivalent protection. ### Attack Path 1. The Skill runs under a permissive or conventional umask and performs a successful login. 2. `saveState()` creates `auth-state.json` without an explicit mode. 3. Another local user enumerates or directly reads the predictable ...[truncated 593 chars]
Remediation
## Remediation Suggestions - Create `STATE_DIR` and `CONFIG_DIR` with mode `0700`. - Create or replace `STATE_FILE` with mode `0600`. - Open the file using restrictive flags and explicitly call `chmodSync(STATE_FILE, 0o600)` after replacement so existing permissive files are repaired. - Write through a securely created temporary file in the same directory, set its mode to `0600`, flush it, and atomically rename it over the state file. - Validate that an overridden `TRIFLE_AUTH_STATE` points to an acceptable user-owned location and reject symbolic links or non-regular files. - On startup, inspect ownership and permissions and refuse to use a state file readable or writable by group or other users. - Provide a logout command that deletes the local token and, where supported, revokes it server-side.

T09 · Insecure Skill Coding Practices

Warning
Location
trifle-auth.mjs:271
Finding
Generated Ethereum private key is exposed in the 1Password process argument list## Vulnerability Details **File Location**: `trifle-auth.mjs`, lines 271-291 **Vulnerability Type**: Private-key disclosure through command-line arguments **Risk Level**: Medium ```javascript async function cmdGenerate() { const privateKey = '0x' + randomBytes(32).toString('hex'); const account = privateKeyToAccount(privateKey); console.log('=== New Wallet Generated ==='); console.log(`Address: ${account.address}`); console.log(''); // Attempt to save directly to 1Password (never prints key to stdout/logs) const opResult = spawnSync( 'op', [ 'item', 'create', '--category', 'Login', '--title', 'EVM Wallet - Trifle Agent', '--vault', 'Gigi', `private_key=${privateKey}`, // nocheck — passed directly to op CLI, never logged `address=${account.address}`, ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] } ); ``` ### Technical Analysis Although the key is not printed to standard output, it is placed directly in the argument vector of the `op` process. Process arguments may be observable through process-inspection facilities such as `/proc/<pid>/cmdline`, process-monitoring utilities, endpoint telemetry, audit logs, crash diagnostics, and administrative process listings. The comment that the value is “never logged” does not account for operating-system and monitoring visibility of command-line arguments. Secret values should be transferred through standard input or another secret-specific input mechanism, not through `argv`. ### Attack Path 1. The victim runs `node trifle-auth.mjs generate`. 2. The Skill starts the `op item create` process with `private_key=0x...` in its arguments. 3. A local monitoring process, sufficiently privileged local user, administrative agent, or telemetry collector observes the process command line during execution. 4. The observer extracts the Ethereum private key. 5. The attacker imports the ke ...[truncated 506 chars]
Remediation
## Remediation Suggestions - Use a 1Password CLI input mechanism that accepts item data through standard input. - Pass a JSON template or supported item document to `op` through `stdin`, keeping the private key out of `argv`. - Avoid placing the secret in environment variables for the child process because those may also be inspectable. - Minimize the lifetime of the key in memory and overwrite mutable secret buffers where practical. - Document the trust boundary and warn if process tracing or endpoint command-line collection is enabled. - Add a regression test that inspects the spawned command arguments and verifies that no private key is present.

T09 · Insecure Skill Coding Practices

Warning
Location
trifle-auth.mjs:254
Finding
Predictable temporary token file allows local pre-creation attacks and unreliable token transfer## Vulnerability Details **File Location**: `trifle-auth.mjs`, lines 254-269 **Vulnerability Type**: Unsafe predictable temporary file containing a bearer token **Risk Level**: Medium ```javascript async function cmdToken() { const state = loadState(); if (!state.token) { console.error('Not authenticated. Run: trifle-auth.mjs login'); process.exit(1); } // Write token to a restricted temp file rather than stdout to avoid log exposure. // Callers read the file: TOKEN=$(cat $(node trifle-auth.mjs token)) const tmpFile = join(tmpdir(), `trifle-token-${process.pid}.tmp`); writeFileSync(tmpFile, state.token, { mode: 0o600 }); // Clean up on exit process.on('exit', () => { try { unlinkSync(tmpFile); } catch {} }); // Print only the file path to stdout process.stdout.write(tmpFile); } ``` ### Technical Analysis The path is derived only from the process ID and is therefore predictable. `writeFileSync()` uses normal overwrite behavior rather than exclusive creation. If the path already exists, the supplied `mode: 0o600` does not repair its existing permissions. An attacker who can predict or monitor the process ID can pre-create a permissive regular file at that path and later read the token written into it. Symbolic-link behavior may provide an additional file-clobber primitive on systems without effective protected-symlink controls. The implementation does not check file ownership, type, or prior existence. Cleanup is also registered for process exit. The documented command substitution waits for the Node.js process to terminate before invoking `cat`; by that point, the exit handler has normally deleted the path. This makes the intended secure handoff inherently racy or nonfunctional and may encourage callers to implement less secure workarounds. ### Attack Path 1. A local attacker predicts upcoming process identifiers or monitors creation of the token command. 2. The attacker pr ...[truncated 889 chars]
Remediation
## Remediation Suggestions - Prefer a documented inter-process handoff mechanism that does not persist the bearer token in a shared temporary directory. - If a file is required, create a private temporary directory with `mkdtempSync()`, mode `0700`, and place the token in a randomly named file with mode `0600`. - Open the token file with exclusive creation semantics such as `O_CREAT | O_EXCL | O_WRONLY` and reject pre-existing paths. - Verify that the created object is a regular file owned by the current user and is not a symbolic link. - Define a cleanup lifecycle that allows the caller to consume the token before deletion, such as an explicit cleanup command or a short, documented expiration period. - Avoid documenting shell command substitution that cannot read the file after the producing process exits. - Where possible, redesign other Skills to read the protected state through a narrowly scoped local API rather than exporting the raw JWT.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (15)

Known Vulnerable Dependency: ws==8.18.3 — 2 advisory(ies): CVE-2026-45736 (ws: Uninitialized memory disclosure); CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
97% confidence
Finding
The lockfile pins viem's transitive dependency to ws 8.18.3, which the supplied advisory data identifies as affected by an uninitialized memory disclosure and a memory-exhaustion denial-of-service issue. In an authentication skill, WebSocket traffic may be reachable during wallet/provider interactions, so a vulnerable ws version can expose sensitive process memory or allow remote service disruption.

Known Vulnerable Dependency: ws==8.17.1 — 2 advisory(ies): CVE-2026-45736 (ws: Uninitialized memory disclosure); CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
96% confidence
Finding
The lockfile also includes ws 8.17.1 via ethers, and the provided advisories mark this version as vulnerable to memory disclosure and memory-exhaustion DoS. Because this skill handles authentication/session material, any dependency that can leak memory or be remotely crashed raises the risk of exposing tokens or interrupting login flows.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill documents capabilities that access environment variables and make external network requests, but it does not declare any tool scope or permission boundaries. In an agent setting, this weakens user visibility and policy enforcement, making it easier for a skill handling secrets and authentication tokens to access sensitive data or external services without explicit approval.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The documentation says generated keys are saved to one 1Password item, but later says the skill reads from a different 1Password path. For an authentication skill managing private keys, this inconsistency can cause operators to store secrets in the wrong place, trigger insecure fallback handling, or accidentally use the wrong wallet for signing.

Description-Behavior Mismatch

Medium
Confidence
89% confidence
Finding
The skill is presented as an authentication helper, but the documentation also includes balance checking and in-app economy data retrieval. This scope expansion increases the chance that users or orchestration systems invoke it in contexts beyond login, exposing additional account data and enabling actions adjacent to game/economy workflows that were not clearly disclosed by the skill's primary purpose.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The manifest defines a post-install hook that executes shell commands (`chmod +x trifle-auth.mjs && npm install`) during installation, which causes code and dependency actions to run automatically on the user's machine. Even though the commands appear routine, automatic install-time execution expands the attack surface and can lead to unexpected code execution or dependency-script execution without explicit user awareness.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The code retrieves a private key from a hard-coded 1Password path tied to a specific personal vault/item. This can cause the skill to silently use an unintended secret in environments where that vault path resolves, creating accidental cross-account authentication and unauthorized use of someone else’s wallet credential.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The manifest describes this skill as handling SIWE authentication, JWT storage, and session management. However, the code also generates entirely new Ethereum private keys and persists them into 1Password or a local key file, which is broader key-management functionality rather than authentication state handling.

Vague Triggers

Low
Confidence
82% confidence
Finding
This markdown file describes the skill's purpose in general terms such as authenticating with the Trifle API and managing wallet-based authentication, but it does not specify how or when the skill should be invoked versus not invoked. Without explicit trigger scope or exclusion conditions, an orchestrator could match the skill too broadly in contexts involving generic authentication or wallet tasks.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"author": "",
  "license": "ISC",
  "dependencies": {
    "siwe": "^3.0.0",
    "viem": "^2.45.1"
  }
}
Confidence
91% confidence
Finding
The dependency version uses a caret range, which permits automatic adoption of future compatible releases rather than a single fixed version. This increases supply-chain risk because a newly published compromised or breaking upstream version could be installed in different environments without explicit review, which matters for an authentication skill handling SIWE flows and token/session logic.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "ISC",
  "dependencies": {
    "siwe": "^3.0.0",
    "viem": "^2.45.1"
  }
}
Confidence
91% confidence
Finding
The viem dependency is specified with a caret range, allowing installation of newer patch/minor versions that have not been explicitly vetted. In a wallet-based authentication skill, this can expose the package to avoidable supply-chain risk if an upstream release introduces malicious code, vulnerable behavior, or authentication/signing regressions.

Intent-Code Divergence

Low
Confidence
96% confidence
Finding
The file header says the `token` command 'Print[s] current JWT token', and the default help says 'Print current JWT token (for piping)'. In reality, `cmdToken` writes the token to a temp file and prints only the file path, so the inline documentation actively misdescribes what the command returns.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The code is hard-wired to read a private key from a specific 1Password path, "op://Gigi/EVM Wallet - Gigi/private_key", which assumes a particular user's vault naming and setup. This is a natural-language policy/context issue because the skill imposes a fixed personal environment convention without offering user choice or documenting it as a justified constraint.

Intent-Code Divergence

Low
Confidence
92% confidence
Finding
The generate command tells users to set `TRIFLE_KEY` or update the op:// path, while the code actually reads `TRIFLE_PRIVATE_KEY`. Elsewhere, the setup text says to store the key as 'Trifle Bot Wallet', but `getPrivateKey` reads from 'EVM Wallet - Gigi' and generation writes 'EVM Wallet - Trifle Agent', creating active documentation-to-code contradictions.

Description-Behavior Mismatch

Low
Confidence
84% confidence
Finding
The manifest frames the skill as an authentication helper for the Trifle API, but the code also queries and displays ball balance via authenticated and public endpoints. Fetching application-specific account data is not clearly part of JWT/session management and expands the operational scope beyond pure auth.