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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Vague Triggers

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
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.

Content

Scanner excerpt · package.json (reported line 13)May include surrounding context.

json
"author": "",
  "license": "ISC",
  "dependencies": {
    "siwe": "^3.0.0",
    "viem": "^2.45.1"
  }
}

Unpinned Dependencies

Low
Category
Supply Chain
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.

Content

Scanner excerpt · package.json (reported line 14)May include surrounding context.

json
"license": "ISC",
  "dependencies": {
    "siwe": "^3.0.0",
    "viem": "^2.45.1"
  }
}

Intent-Code Divergence

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.