Back to skill

Security audit

M365 Spam Manager

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it claims at a high level, but its mailbox-changing commands can run without the confirmations its documentation promises.

Review this skill carefully before installing. It is not showing clear malicious intent, but it can alter Outlook or Exchange mailbox state without the confirmations its documentation describes. Use only with a Microsoft profile you trust, avoid running undocumented bulk commands unless you understand the effects, and prefer dry-run or manual review workflows until the move and labeling commands require explicit approval.

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

Warning
Location
scripts/move-to-inbox.mjs:20
Finding
Message is moved to Inbox without the documented user confirmation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/move-to-inbox.mjs:20-29` **Related Documentation**: `SKILL.md:28-29, 71-73` **Vulnerability Type**: Missing authorization confirmation for a mailbox mutation **Risk Level**: Medium ### Vulnerable Code ```js // Move to Inbox (POST to move) const url = `${base}/messages/${encodeURIComponent(id)}/move`; const result = await graphFetch(url, { method: 'POST', token, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ destinationId: inbox.id }), }); console.log(`✅ Moved message ${id} to Inbox`); console.log(` New location: ${inbox.displayName}`); ``` The documented policy states: ```md # Move a false positive to inbox (requires confirmation) ... In review mode, the script always prompts for confirmation before moving emails. ``` ### Technical Analysis The script obtains a `Mail.ReadWrite` token and immediately submits the Microsoft Graph `move` operation after parsing the command-line arguments. It does not prompt the user, require a `--yes` or `--automatic` option, or otherwise verify that the user has approved the specific mailbox, message, and destination. This contradicts the Skill's declared controlled-mutation policy. Although `Mail.ReadWrite` is functionally necessary to move a message, the lack of an approval boundary allows the permission to be exercised without the documented user interaction. ### Attack Path 1. An Agent, automation process, or user invokes `move-to-inbox.mjs` with a profile, mailbox, and message ID. 2. The caller expects a confirmation prompt because the Skill documentation says one is required. 3. The script loads the cached Microsoft 365 credential and requests a `Mail.ReadWrite` token. 4. It locates the Inbox and immediately sends `POST /messages/{id}/move`. 5. The selected message is moved before the user has an opportunity to review or reject the action. An attacker who can influence the message ID or generated command could ther ...[truncated 506 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prompt for confirmation by default and display the mailbox, message ID, source action, and destination before executing the request. 2. Require an explicit non-interactive option such as `--yes` or `--automatic` to bypass the prompt. 3. Refuse non-interactive execution without that explicit option. 4. Consider retrieving and displaying the message sender and subject before approval so the user can validate the selected item. 5. Update the documentation to describe the exact confirmation and automation controls. 6. Record a concise audit event after approval without logging access tokens or message content unnecessarily. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/move-to-learning.mjs:20
Finding
Learning-folder creation and message movement occur without user confirmation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/move-to-learning.mjs:20-44` **Related Documentation**: `SKILL.md:31-32, 71-73` **Vulnerability Type**: Unconfirmed mailbox folder creation and message movement **Risk Level**: Medium ### Vulnerable Code ```js if (!learning) { // Create it if it doesn't exist const created = await graphFetch(`${base}/mailFolders`, { method: 'POST', token, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'Junk Examples' }), }); console.log(`📁 Created learning folder: "Junk Examples"`); var learningFolderId = created.id; } else { var learningFolderId = learning.id; console.log(`📁 Using existing learning folder: "${learning.displayName}"`); } // Move to learning folder const url = `${base}/messages/${encodeURIComponent(id)}/move`; const result = await graphFetch(url, { method: 'POST', token, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ destinationId: learningFolderId }), }); console.log(`✅ Moved message ${id} to learning folder`); ``` ### Technical Analysis The command performs as many as two mailbox mutations: it creates the `Junk Examples` folder when absent and then moves the specified message into it. Both operations are performed immediately with `Mail.ReadWrite`; neither is protected by an interactive confirmation or explicit unattended-mode flag. The documentation declares mailbox moves to be controlled actions and says review mode always prompts before moving messages. The implementation does not enforce that policy. ### Attack Path 1. A caller invokes the command with a profile and attacker-influenced or incorrectly selected message ID. 2. The script acquires a Microsoft Graph `Mail.ReadWrite` token. 3. If the learning folder is absent, the script creates it without approval. 4. The script immediately sends the move request for the selected message. 5. The message leaves its current folder without ...[truncated 422 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add a default confirmation prompt covering both possible operations: folder creation and message movement. 2. Require an explicit `--yes` or `--automatic` flag for unattended execution. 3. Ask separately before creating a missing folder, or include folder creation clearly in a single transaction summary. 4. Validate and display the mailbox, message metadata, and destination folder before approval. 5. Where possible, verify that the message currently resides in the expected Junk folder before moving it. 6. Document the behavior of the learning folder and any downstream processing associated with placing messages there. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/check-spam.mjs:6
Finding
Spam-check command defaults to live bulk mutations and modifies categories during dry-run mode<![CDATA[ ## Vulnerability Details **File Location**: `scripts/check-spam.mjs:6, 58-86, 120-137` **Vulnerability Type**: Unsafe default behavior and incomplete dry-run isolation **Risk Level**: Medium ### Vulnerable Code ```js const dryRun = getArg('dryRun', 'false') === 'true'; ``` The category creation runs before the dry-run check: ```js // Get or create categories const categoriesUrl = `${base}/outlook/masterCategories`; const categoriesRes = await graphFetch(categoriesUrl, { token }); const existingCats = categoriesRes.value || []; const spamCat = existingCats.find(c => c.displayName === 'Spam'); const okCat = existingCats.find(c => c.displayName === 'OK'); let spamCatId = spamCat?.id; let okCatId = okCat?.id; if (!spamCatId) { const created = await graphFetch(categoriesUrl, { method: 'POST', token, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'Spam', color: 'preset0' }), }); spamCatId = created.id; console.log('✅ Created category: Spam'); } if (!okCatId) { const created = await graphFetch(categoriesUrl, { method: 'POST', token, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ displayName: 'OK', color: 'preset7' }), }); okCatId = created.id; console.log('✅ Created category: OK'); } ``` Only message labeling is conditionally suppressed: ```js if (dryRun) { console.log(`[DRY RUN] Would label "${m.subject?.slice(0,40)}..." as ${label} (score: ${score})`); if (isSpam) spamCount++; else okCount++; continue; } // Update categories const newCats = [...existing, label]; await graphFetch(`${base}/messages/${encodeURIComponent(m.id)}`, { method: 'PATCH', token, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ categories: newCats }), }); if (isSpam) spamCount++; else okCount++; console.log(`✅ Labeled: ${label} (score ${score}) - ${m.subject?.slice(0,40)}...`); ``` ### Technical Analysis The command defaults ...[truncated 1767 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Default to dry-run mode: ```js const apply = hasFlag('apply'); ``` Perform writes only when the caller explicitly supplies `--apply`. 2. Move every mutation, including master-category creation, behind the same apply/confirmation boundary. 3. In interactive mode, display a summary of proposed category creation and message changes, then request confirmation once before applying them. 4. Require an explicit unattended flag for automation and document that it bypasses confirmation. 5. Return a non-mutating plan in dry-run mode, including which categories would be created. 6. Document `check-spam.mjs`, its permissions, its maximum batch size, its threshold semantics, and all mutating behavior. 7. Consider requiring review for borderline scores and retaining enough metadata to reverse bulk changes. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/analyze.mjs:148
Finding
Attacker-controlled email fields are printed without terminal control-character sanitization<![CDATA[ ## Vulnerability Details **File Locations**: - `scripts/analyze.mjs:148-157` - `scripts/check-spam.mjs:121-123, 138-139` **Vulnerability Type**: Terminal escape-sequence and output-spoofing risk **Risk Level**: Low ### Vulnerable Code From `scripts/analyze.mjs`: ```js const level = score <= 30 ? '🟢' : score <= 70 ? '🟡' : '🔴'; console.log(level + ' [' + score + '] ' + from); console.log(' Subject: ' + (m.subject || '').slice(0, 60)); if (reasons.length > 0) { console.log(' Reasons: ' + reasons.join(', ')); } console.log(''); ``` From `scripts/check-spam.mjs`: ```js if (dryRun) { console.log(`[DRY RUN] Would label "${m.subject?.slice(0,40)}..." as ${label} (score: ${score})`); if (isSpam) spamCount++; else okCount++; continue; } ``` ```js console.log(`✅ Labeled: ${label} (score ${score}) - ${m.subject?.slice(0,40)}...`); ``` ### Technical Analysis Message senders and subjects originate from received email and must be treated as untrusted. The scripts interpolate these values directly into terminal output. Applying `slice()` limits length but does not remove ANSI escape sequences or C0/C1 control characters. If such characters survive email processing and Microsoft Graph normalization, a compatible terminal may interpret them rather than display them literally. This can alter colors, move or erase the cursor, create deceptive hyperlinks, or visually overwrite surrounding audit results. This issue affects presentation integrity rather than granting direct code execution in the reviewed implementation. ### Attack Path 1. An attacker sends an email with a crafted sender or subject containing terminal control or ANSI escape sequences. 2. The message reaches the target mailbox's Junk folder. 3. The user runs the analyzer or spam-check command in a terminal. 4. Microsoft Graph returns the crafted field, assuming it survives upstream normalization. 5. The script prints it without escaping. 6. The terminal interprets the sequence, allow ...[truncated 469 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Sanitize every untrusted email-derived value before terminal output. 2. Remove ANSI escape sequences and escape or replace C0/C1 control characters, while optionally preserving safe whitespace. 3. Use a centralized output helper so sender addresses, subjects, folder display names, and Graph error text receive consistent treatment. 4. Consider rendering untrusted strings with `JSON.stringify()` or a dedicated terminal-escaping library. 5. Add tests containing ESC, BEL, carriage return, backspace, newline, OSC hyperlinks, and cursor-control sequences. 6. Keep raw values only for internal scoring; never print them directly to an interactive terminal. ]]>
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
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (7)

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
node skills/m365-spam-manager/scripts/move-to-learning.mjs --profile tom-business-mail --mailbox radman@e-ola.com --id <MSG_ID>
```

### Automatic mode (no confirmation)

```bash
# Auto-clean: move high-confidence spam to learning, medium to review
Confidence
86% confidence
Finding
This is a true autonomous-decision risk because the skill advertises 'Automatic mode (no confirmation)' for taking mailbox actions based on heuristic spam scoring rather than deterministic truth. The skill context makes this more dangerous, not less, because email classification is error-prone and the affected resource is a real Exchange mailbox, including potentially shared mailboxes.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill explicitly supports an automatic mode that performs mailbox-moving actions without per-item confirmation, which can modify a user's mailbox state at scale. In the context of email security workflows, false positives are common, so unattended moves can misclassify legitimate mail and cause loss of visibility, missed business communications, or training-folder pollution.

Missing User Warnings

Medium
Confidence
81% confidence
Finding
This code persists the MSAL token cache back to the local filesystem with fs.writeFileSync, which is a safety-relevant file write involving authentication material. In this file there is no confirmation prompt, logging, or comment warning the user that token data may be updated on disk.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The graphFetch helper performs outbound HTTP requests and may attach an Authorization bearer token, which can transmit user or system data to remote services. This file contains no confirmation, logging, or explanatory comment warning that requests and credentials may be sent over the network.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This script accesses mailbox contents via Microsoft Graph and prints sender addresses, subjects, folder names, and risk annotations directly to stdout without any built-in warning, consent prompt, masking, or output-safety controls. In an agent/skill context, that can expose sensitive email metadata to logs, calling processes, terminals, or downstream tools, increasing the chance of unintended disclosure of private communications.

Known Vulnerable Dependency: uuid==8.3.2 — 1 advisory(ies): CVE-2026-41907 (uuid: Missing buffer bounds check in v3/v5/v6 when buf is provided)

Low
Category
Supply Chain
Confidence
87% confidence
Finding
The lockfile pins uuid to 8.3.2, which the supplied finding identifies as affected by a bounds-check issue in v3/v5/v6 when a caller provides a buf argument. Although package-lock.json alone does not prove the vulnerable API path is exercised, shipping a dependency version with a known security advisory is generally a real supply-chain risk because downstream code or transitive code may invoke the affected functions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"type": "module",
  "description": "OpenClaw skill: Microsoft 365 spam folder manager with suspicious score calculation.",
  "dependencies": {
    "@azure/msal-node": "^5.0.4"
  }
}
Confidence
88% confidence
Finding
The dependency version is specified with a caret range (^5.0.4), which permits automatic installation of newer minor and patch releases. This weakens build reproducibility and can unintentionally introduce a compromised or breaking upstream release into the skill's authentication flow, especially significant because this package handles Microsoft identity and token management.

Static analysis

No suspicious patterns detected.