Back to skill

Security audit

Nadmail

Security checks for vulnerabilities and agentic risk

Overview

NadMail is a coherent email-and-wallet skill, but it needs Review because it handles wallet secrets and financial actions with some weak safeguards.

Install only if you are comfortable giving this skill access to a NadMail wallet identity, storing a NadMail auth token locally, and sending email/inbox data to api.nadmail.ai. Use a dedicated low-balance wallet, do not rely on the local emo-buy cap as a hard financial limit, avoid entering wallet passwords in recorded/shared terminals, and update dependencies before serious use.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (3)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/register.js:84
Finding
Wallet path validation can escape the user's home directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/register.js:84-114, 133-137` **Vulnerability Type**: Improper path containment validation and symlink traversal **Risk Level**: Medium ### Vulnerable Code ```javascript function validateWalletPath(walletPath) { const resolved = path.resolve(walletPath); // Must be under home directory (prevent reading system files) const home = process.env.HOME; if (!resolved.startsWith(home)) { console.error(`Security: Wallet path must be under your home directory (${home})`); process.exit(1); } // Check for suspicious path components if (walletPath.includes('..') || walletPath.includes('\0')) { console.error('Security: Invalid path — must not contain ".." or null bytes'); process.exit(1); } // Check file size try { const stat = fs.statSync(resolved); if (stat.size > MAX_WALLET_FILE_SIZE) { console.error(`Security: Wallet file too large (${stat.size} bytes, max ${MAX_WALLET_FILE_SIZE})`); process.exit(1); } if (!stat.isFile()) { console.error('Security: Path must point to a regular file'); process.exit(1); } } catch (e) { // File doesn't exist — let the caller handle it } return resolved; } ``` The validated path is subsequently read as a private key: ```javascript const walletArg = getArg('--wallet'); if (walletArg) { const walletPath = validateWalletPath(walletArg.replace(/^~/, process.env.HOME)); if (fs.existsSync(walletPath)) { console.log(`Using wallet file: ${walletPath}`); const key = fs.readFileSync(walletPath, 'utf8').trim(); ``` ### Technical Analysis The containment check uses: ```javascript resolved.startsWith(home) ``` String-prefix comparison does not establish that one filesystem path is a child of another. For example, if `$HOME` is `/home/alice`, a path such as `/home/alice-backup/key` begins with the same string and passes the check even though it is outside `/home/alice`. The implem ...[truncated 1766 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Canonicalize both the home directory and selected file using `fs.realpathSync`. 2. Use `path.relative()` rather than string-prefix comparison: ```javascript const realHome = fs.realpathSync(process.env.HOME); const realFile = fs.realpathSync(resolved); const relative = path.relative(realHome, realFile); if ( relative === '' || relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative) ) { throw new Error('Wallet file must be located inside the home directory'); } ``` 3. Use `fs.lstatSync()` before following the path and reject symbolic links if symlink-based wallet files are not explicitly required. 4. Open the file only after validation, preferably using a file descriptor and platform-supported anti-symlink flags to reduce time-of-check/time-of-use races. 5. Verify that the file is owned by the current user and reject files writable by group or other users. 6. Replace `process.exit()` inside validation helpers with exceptions so callers can handle failures consistently and securely. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/setup.js:34
Finding
Wallet encryption passwords are entered with terminal echo enabled<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.js:34-44, 204-213`; `scripts/register.js:37-47, 165` **Vulnerability Type**: Sensitive input exposure through an echoing terminal prompt **Risk Level**: Medium ### Vulnerable Code The setup script defines a standard `readline` prompt: ```javascript function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise(resolve => { rl.question(question, answer => { rl.close(); resolve(answer.trim()); }); }); } ``` It uses that prompt for both password entry and confirmation: ```javascript let password; while (true) { password = await prompt('\nSet encryption password (min 8 chars, must include letter + number): '); const validation = validatePassword(password); if (validation.valid) break; console.error('Invalid password:'); validation.errors.forEach(e => console.error(` - ${e}`)); } const confirmPwd = await prompt('Confirm password: '); ``` The registration script uses the same echoing prompt implementation: ```javascript function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise(resolve => { rl.question(question, answer => { rl.close(); resolve(answer.trim()); }); }); } ``` It then requests the decryption password through that prompt: ```javascript const password = process.env.NADMAIL_PASSWORD || await prompt('Enter wallet password: '); ``` ### Technical Analysis Node.js `readline.question()` echoes typed input to the configured output stream by default. The code does not replace the terminal output handler, disable echo, or use a dedicated secret-input library. As a result, encryption and decryption passwords are visibly displayed while entered. This undermines protection of the AES-256-GCM-encrypted private key because possession of both `~/.nadmail/private ...[truncated 1419 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace `readline.question()` for secret entry with a well-maintained no-echo password prompt. 2. If implementing no-echo input directly, require a TTY, disable terminal echo only for the duration of entry, and restore terminal state in a `finally` block. 3. Avoid printing secret values or including them in errors, audit logs, or debug output. 4. Document the risks of `NADMAIL_PASSWORD` and recommend a protected secret manager or file descriptor rather than persistent shell environment configuration. 5. Clear password references as soon as practical after key derivation. Although JavaScript cannot guarantee memory erasure for immutable strings, minimizing lifetime still reduces accidental exposure. 6. Add automated tests verifying that password characters are not written to `stdout` or `stderr`. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/send.js:74
Finding
Daily financial spending limit is mutable and vulnerable to concurrent bypass<![CDATA[ ## Vulnerability Details **File Location**: `scripts/send.js:30, 74-132, 220-252, 303-305` **Vulnerability Type**: Non-atomic client-side enforcement of a financial transaction limit **Risk Level**: Medium ### Vulnerable Code The cap is controlled by a local environment variable: ```javascript const DEFAULT_EMO_DAILY_CAP = 0.5; const EMO_DAILY_CAP = parseFloat(process.env.NADMAIL_EMO_DAILY_CAP) || DEFAULT_EMO_DAILY_CAP; ``` The limit check reads local state without locking or reserving funds: ```javascript function checkEmoDailyLimit(emoAmount) { const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD let tracker = { date: today, total: 0 }; try { if (fs.existsSync(EMO_TRACKER_FILE)) { tracker = JSON.parse(fs.readFileSync(EMO_TRACKER_FILE, 'utf8')); if (tracker.date !== today) { tracker = { date: today, total: 0 }; } } } catch { tracker = { date: today, total: 0 }; } const newTotal = tracker.total + emoAmount; if (newTotal > EMO_DAILY_CAP) { return { allowed: false, spent_today: tracker.total, remaining: Math.max(0, EMO_DAILY_CAP - tracker.total), cap: EMO_DAILY_CAP, }; } return { allowed: true, spent_today: tracker.total, remaining: EMO_DAILY_CAP - tracker.total, cap: EMO_DAILY_CAP, }; } ``` Spending is recorded through a separate read-modify-write operation: ```javascript function recordEmoSpend(emoAmount) { const today = new Date().toISOString().slice(0, 10); let tracker = { date: today, total: 0 }; try { if (fs.existsSync(EMO_TRACKER_FILE)) { tracker = JSON.parse(fs.readFileSync(EMO_TRACKER_FILE, 'utf8')); if (tracker.date !== today) { tracker = { date: today, total: 0 }; } } } catch { tracker = { date: today, total: 0 }; } tracker.total += emoAmount; fs.writeFileSync(EMO_TRACKER_FILE, JSON.stringify(tracker, null, 2), { mode: 0o600 }); } ``` The transaction is checke ...[truncated 3027 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce a non-bypassable absolute daily limit on the server for each authenticated wallet or account. 2. Implement the server-side check and spend reservation as one atomic transaction before initiating the blockchain purchase. 3. Use idempotency keys so retries or ambiguous network responses cannot create duplicate purchases. 4. Treat local tracking only as defense-in-depth and user-facing accounting, not as the authoritative financial control. 5. If local enforcement is retained, use an inter-process lock and atomic file replacement: - Acquire an exclusive lock. - Read and validate the tracker. - Reserve the pending amount. - Write to a temporary file with mode `0600`. - Flush and atomically rename it. - Release or reconcile the reservation after the server response. 6. Validate the configured cap strictly: ```javascript const configuredCap = Number(process.env.NADMAIL_EMO_DAILY_CAP); const EMO_DAILY_CAP = Number.isFinite(configuredCap) && configuredCap > 0 ? configuredCap : DEFAULT_EMO_DAILY_CAP; ``` 7. Include both the standard micro-buy and optional emo-buy in displayed and authoritative spending controls if the cap is intended to represent total financial exposure. 8. Reconcile local records against server-side transaction history after timeouts or malformed responses. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (36)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The declared description centers on NadMail as an email platform with registration, email sending, and investment-related messaging features. This code chunk instead performs wallet provisioning and secure local key management. While a wallet may be a supporting prerequisite for a Monad/crypto-based service, the actual behavior here is materially different from the declared end-user purpose: it creates and encrypts a wallet, stores files in ~/.nadmail, cleans up legacy sensitive files, prints a mnemonic, and logs audit entries. Those are significant operational capabilities not reflected in the declared description, and the advertised email/micro-investing features are absent from the code shown.

Ae1

High
Category
analysis-evasion
Content
node scripts/register.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/register.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/register.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/register.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/register.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/register.js
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/send.js "friend@nadmail.ai" "Hello!" "Nice to meet you"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/send.js "friend@nadmail.ai" "Hello!" "Nice to meet you"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/send.js "friend@nadmail.ai" "Hello!" "Nice to meet you"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/send.js "friend@nadmail.ai" "Hello!" "Nice to meet you"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/send.js "friend@nadmail.ai" "Hello!" "Nice to meet you"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `audit.js` | View audit log | No |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `audit.js` | View audit log | No |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `audit.js` | View audit log | No |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

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
97% confidence
Finding
The lockfile pins ws to version 8.17.1, and the supplied advisory data indicates this version is affected by an uninitialized memory disclosure and a memory-exhaustion denial-of-service issue. Even though ws is an indirect dependency via ethers, it can still be reachable anywhere the skill uses websocket-based blockchain connections, which is plausible for a Monad/email-on-chain integration.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill documentation describes use of environment variables, local credential files, and remote API calls, but it does not declare any tool scope such as permissions or allowed-tools. For an agent skill, undeclared access to env and network increases the risk that a caller grants broader capabilities than expected, especially because the skill handles wallet secrets and bearer tokens.

External Transmission

Medium
Category
Data Exfiltration
Content
2. Submit the transaction hash:
   ```bash
   curl -X POST https://api.nadmail.ai/api/credits/buy \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"tx_hash": "0xYOUR_TX_HASH"}'
Confidence
87% confidence
Finding
The skill instructs agents to transmit authentication tokens and blockchain transaction hashes to an external API. Any skill that sends sensitive identifiers off-host creates exposure to token theft, account misuse, metadata leakage, or interaction with an untrusted service if the endpoint or transport assumptions are wrong.

External Transmission

Medium
Category
Data Exfiltration
Content
2. Submit the transaction hash:
   ```bash
   curl -X POST https://api.nadmail.ai/api/credits/buy \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"tx_hash": "0xYOUR_TX_HASH"}'
Confidence
87% confidence
Finding
The skill instructs agents to transmit authentication tokens and blockchain transaction hashes to an external API. Any skill that sends sensitive identifiers off-host creates exposure to token theft, account misuse, metadata leakage, or interaction with an untrusted service if the endpoint or transport assumptions are wrong.

External Transmission

Medium
Category
Data Exfiltration
Content
### Check Balance

```bash
curl https://api.nadmail.ai/api/credits \
  -H "Authorization: Bearer YOUR_TOKEN"
```
Confidence
82% confidence
Finding
The documented balance-check request sends a bearer token to an external API, which means the skill depends on remote service trust and proper token handling. In an agent setting, this can leak credentials through process history, logs, or overly broad network permissions if not tightly controlled.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```
~/.nadmail/
├── private-key.enc   # Encrypted private key (AES-256-GCM, chmod 600)
├── wallet.json       # Wallet info (public address only)
├── token.json        # Auth token (chmod 600)
├── emo-daily.json    # Daily emo-buy spending tracker (chmod 600)
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```
~/.nadmail/
├── private-key.enc   # Encrypted private key (AES-256-GCM, chmod 600)
├── wallet.json       # Wallet info (public address only)
├── token.json        # Auth token (chmod 600)
├── emo-daily.json    # Daily emo-buy spending tracker (chmod 600)
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```
~/.nadmail/
├── private-key.enc   # Encrypted private key (AES-256-GCM, chmod 600)
├── wallet.json       # Wallet info (public address only)
├── token.json        # Auth token (chmod 600)
├── emo-daily.json    # Daily emo-buy spending tracker (chmod 600)
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

External Transmission

Medium
Category
Data Exfiltration
Content
- Website: https://nadmail.ai
- API: https://api.nadmail.ai
- API Docs: https://api.nadmail.ai/api/docs

---
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This code sends an authorization token to a remote API to retrieve inbox contents, which necessarily involves transmitting sensitive mailbox data. While the file has a basic usage comment, it does not explicitly warn users that running the script contacts a remote service and accesses private email data.

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal, suspicious.potential_exfiltration

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/inbox.js:14

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/register.js:22

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/send.js:22

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/register.js:169

Sensitive-looking file read is paired with a network send.

Warn
Code
suspicious.potential_exfiltration
Location
scripts/inbox.js:42

Sensitive-looking file read is paired with a network send.

Warn
Code
suspicious.potential_exfiltration
Location
scripts/send.js:135