Back to skill

Security audit

Microsoft 365

Security checks for vulnerabilities and agentic risk

Overview

This looks like a real Microsoft 365 integration, but it asks for broad write and offline access and stores reusable OAuth tokens in plaintext with inconsistent documentation.

Review carefully before installing. Use only if you are comfortable granting broad Microsoft 365 permissions, including sending mail and modifying calendars, contacts, and OneDrive files. Protect ~/.openclaw/credentials/ms365.tokens.<account>.json and ms365.env with owner-only permissions, consider a dedicated low-privilege Microsoft app/account, and revoke the Microsoft app grant if you stop using the skill or suspect token exposure.

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)

T09 · Insecure Skill Coding Practices

Error
Location
src/config.js:128
Finding
OAuth Tokens Are Stored Without Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `src/config.js:128-145` **Vulnerability Type**: Insecure storage of sensitive OAuth credentials **Risk Level**: High ### Vulnerable Code ```js function saveTokens(tokens, account = 'default') { if (!tokens || typeof tokens !== 'object') { throw new Error('saveTokens verwacht een tokens-object.'); } const { tokens: tokenPath } = getPaths(account); const toSave = { ...tokens }; // Houd refresh_token vast als provider hem niet altijd terugstuurt, // deze merge gebeurt in auth.js bij refresh. if (!toSave.expires_at) { const expiresIn = Number(toSave.expires_in || 0); if (Number.isFinite(expiresIn) && expiresIn > 0) { toSave.expires_at = Date.now() + (expiresIn * 1000); } } fs.writeFileSync(tokenPath, JSON.stringify(toSave, null, 2)); } ``` ### Technical Analysis The Skill persists Microsoft OAuth access and refresh tokens using `fs.writeFileSync` without specifying a restrictive file mode. The effective permissions therefore depend on the process umask. On systems with a common `0022` umask, a newly created token file may have mode `0644`, making it readable by other local users. The stored refresh token is a long-lived reusable credential. An attacker who reads it can submit it to Microsoft's OAuth token endpoint and obtain new access tokens without knowing the user's password. The resulting privileges include every delegated scope granted to the application. The implementation also does not verify whether the destination file is a symbolic link, whether it is owned by the current user, or whether an existing file has secure permissions before reading or overwriting it. ### Attack Path 1. A victim authenticates through the device-code flow. 2. The Skill writes the token response to `~/.openclaw/credentials/ms365.tokens.&lt;account&gt;.json`. 3. The token file inherits permissions from the process umask and may be readable by another local account or compromise ...[truncated 938 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the credential directory with owner-only permissions: ```js fs.mkdirSync(credentialsDir, { recursive: true, mode: 0o700 }); ``` 2. Write token files with mode `0600`: ```js fs.writeFileSync(tokenPath, JSON.stringify(toSave, null, 2), { encoding: 'utf8', mode: 0o600, flag: 'w' }); fs.chmodSync(tokenPath, 0o600); ``` 3. Use atomic replacement: - Create a temporary file in the credential directory with `0600`. - Flush and close it. - Rename it over the destination. - Never use a globally writable temporary directory. 4. Before reading or replacing an existing token file: - Use `lstat` to reject symbolic links. - Verify that the file is owned by the current user. - Reject or repair group/world-readable permissions. - Consider opening files with no-follow semantics where supported. 5. Prefer an operating-system credential vault or keychain instead of plaintext JSON when available. 6. Document token revocation procedures and instruct users to revoke the application grant if token disclosure is suspected. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/api.js:12
Finding
Microsoft Bearer Token Can Be Forwarded to an Unrestricted Absolute URL<![CDATA[ ## Vulnerability Details **File Location**: `src/api.js:12-28`, `src/api.js:38-50` **Vulnerability Type**: Authorization-header disclosure through unvalidated pagination URLs **Risk Level**: Medium ### Vulnerable Code ```js async function callGraph(endpoint, method = 'GET', body = null) { const token = await getAccessToken(currentAccount); if (!token) throw new Error('No access token available. Login required.'); const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; const options = { method, headers }; if (body) options.body = JSON.stringify(body); const url = endpoint.startsWith('http') ? endpoint : `${GRAPH_BASE}${endpoint}`; const res = await fetch(url, options); if (!res.ok) { const errText = await res.text(); throw new Error(`Graph API Error (${res.status}): ${errText}`); } ``` ```js async function callGraphAllPages(endpoint) { let next = endpoint; const all = []; while (next) { const data = await callGraph(next); if (Array.isArray(data?.value)) all.push(...data.value); next = data?.['@odata.nextLink'] || null; } return all; } ``` ### Technical Analysis `callGraph` accepts any endpoint beginning with `http` as a complete destination URL. It then sends the Microsoft OAuth bearer token in the `Authorization` header without validating the URL's scheme, hostname, port, or origin. `callGraphAllPages` consumes the `@odata.nextLink` property from a response and passes it directly back to `callGraph`. Under normal operation, Microsoft Graph returns an HTTPS URL on `graph.microsoft.com`. However, the code does not enforce that assumption. If a malicious or altered response supplies an attacker-controlled absolute URL, the next pagination request will disclose the bearer token. Relevant response-manipulation scenarios include a compromised network interception environment, a mocked or monkey-patched `fetch`, a malicious test fixture, or a future code ...[truncated 1400 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Validate every destination before attaching an authorization header. Only the exact Microsoft Graph HTTPS origin should be accepted: ```js function resolveGraphUrl(endpoint) { const url = new URL(endpoint, GRAPH_BASE); if ( url.protocol !== 'https:' || url.hostname !== 'graph.microsoft.com' || url.port !== '' ) { throw new Error('Rejected non-Microsoft Graph URL'); } if (url.username || url.password) { throw new Error('URLs containing credentials are not allowed'); } return url.toString(); } ``` Replace the current resolution logic with: ```js const url = resolveGraphUrl(endpoint); const res = await fetch(url, options); ``` Additional hardening should include: 1. Validate every `@odata.nextLink` before following it. 2. Set a maximum page count to prevent unbounded pagination or denial of service. 3. Do not use a broad `startsWith('http')` check for trusted destinations. 4. Keep bearer-token attachment inside a dedicated Microsoft Graph client that cannot issue authenticated requests to arbitrary origins. 5. Add tests proving that HTTP URLs, lookalike domains, alternate ports, embedded credentials, and attacker-controlled hosts are rejected. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
src/auth.js:4
Finding
OAuth Permissions Exceed the Minimum Required Scope and Are Incompletely Disclosed<![CDATA[ ## Vulnerability Details **File Location**: `src/auth.js:4`, `README.md:5-8`, `README.md:44-55`, `README.md:59-64` **Vulnerability Type**: Excessive delegated OAuth privileges and inaccurate security documentation **Risk Level**: Medium ### Vulnerable Code and Documentation The runtime always requests the following combined scope set: ```js const SCOPES = 'User.Read Mail.Read Mail.Send Calendars.ReadWrite Contacts.ReadWrite Files.ReadWrite.All offline_access'; ``` The README characterizes the integration primarily as read-oriented and states a different token location: ```md ## Veiligheid - **Lokaal**: Alle tokens worden lokaal opgeslagen in `tokens.json`. - **Transparant**: De code is open source en maakt directe calls naar Microsoft Graph. - **Geen externe proxy**: Gebruikt de Device Code Flow, direct tussen uw machine en Microsoft. ``` ```md 5. Het script pikt automatisch de login op en slaat de tokens veilig op in `tokens.json`. ``` ```md ## Functies Het script ondersteunt momenteel: - Recente e-mails lezen - Agenda-items ophalen - Contacten inzien - Bestanden in de root van OneDrive bekijken ``` The actual token path is account-specific: ```js return { config: path.join(__dirname, `../config.${accountName}.json`), tokens: path.join(credentialsDir, `ms365.tokens.${accountName}.json`) }; ``` ### Technical Analysis The authentication flow requests all read and write scopes at every login, regardless of which feature the user intends to use. For example, invoking only the `--calendar` read operation still requests permission to send mail, modify contacts, modify calendars, and modify files. `Files.ReadWrite.All` is particularly broad because it can cover files the signed-in user can access, rather than limiting access to only the user's own files where a narrower delegated scope may be sufficient. The declared OneDrive behavior consists of listing files and uploading a sample file to the user's drive; this does not clearly justify ...[truncated 1953 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply incremental consent and request scopes according to the selected feature: - Email listing: `Mail.Read` - Sending email: `Mail.Send` - Calendar listing: `Calendars.Read` - Calendar modification: `Calendars.ReadWrite` - Contact listing: `Contacts.Read` - Contact modification: `Contacts.ReadWrite` - OneDrive listing: `Files.Read` - Uploading to the user's drive: prefer `Files.ReadWrite` where sufficient - Request `offline_access` only when persistent background or cross-session access is necessary. 2. Avoid `Files.ReadWrite.All` unless functionality requiring access to all files available to the user is explicitly implemented and clearly disclosed. 3. Separate read-only and write-capable modes. Require explicit user confirmation before requesting write scopes. 4. Update the README and `SKILL.md` so both accurately document: - Every requested OAuth permission. - Which actions are read-only and which modify data. - The risks associated with `offline_access`. - The actual token location: `~/.openclaw/credentials/ms365.tokens.<account>.json`. - How to remove local tokens and revoke the Microsoft application grant. 5. Update setup and configuration documentation to use the runtime's actual account-specific filenames. `setup.js` currently writes `config.json`, while runtime loading expects `config.<account>.json`. 6. Add automated tests that verify read-only commands do not request unrelated write scopes. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (22)

Credential Access

High
Category
Privilege Escalation
Content
async function callGraph(endpoint, method = 'GET', body = null) {
  const token = await getAccessToken(currentAccount);
  if (!token) throw new Error('No access token available. Login required.');

  const headers = {
    Authorization: `Bearer ${token}`,
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
}
    }
  } catch (err) {
    console.error(`[config] Kon .env niet laden (${envPath}):`, err.message);
  }
}
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
}
    }
  } catch (err) {
    console.error(`[config] Kon .env niet laden (${envPath}):`, err.message);
  }
}
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The README tells users that OAuth tokens are stored locally in `tokens.json` but does not warn them to protect that file, exclude it from version control, or restrict filesystem permissions. Refresh/access tokens can grant ongoing access to mail, calendar, contacts, and OneDrive data if the file is copied, shared, or accidentally committed.

Description-Behavior Mismatch

Medium
Confidence
90% confidence
Finding
The manifest describes broader functionality including sending emails and managing calendar events, while the README's 'Functies' section lists only reading recent emails, fetching calendar items, viewing contacts, and browsing OneDrive root files. This is a semantic mismatch between the declared scope of the skill and the documented behavior of the implementation.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill declares capabilities that require environment-variable access and network communication, but it does not explicitly define a tool scope such as permissions or allowed-tools. This creates ambiguity about what the skill is authorized to do and weakens containment, especially for a skill that accesses Microsoft 365 data and uses stored credentials.

Session Persistence

Medium
Category
Rogue Agent
Content
### Calendar
- **Fetch events**: `node index.js --account personal --calendar`
- **Create event**: Use interactive mode or automated scripts.

### Email
- **Read emails**: Interactive mode (option 1).
Confidence
67% confidence
Finding
The skill explicitly stores authentication tokens under the user's home directory and supports interactive and automated reuse of an account session. Persistent Microsoft 365 tokens increase the blast radius of compromise because anyone who can read those files or trigger the skill under the same user context may gain ongoing access to email, calendar, contacts, and OneDrive data.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The script's user-facing prompts and instructions are written in Dutch, which imposes a specific language on all users. There is no opt-in, fallback, or documented justification that this skill is intended only for Dutch-speaking users or a Dutch-specific environment.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This function sends an email via the Microsoft Graph API, which is an external action affecting user data and communications. In this file there is no confirmation prompt, logging/print statement, or explanatory docstring/comment warning that the function transmits content to Microsoft Graph and causes an email to be sent.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
This function creates calendar events, which writes user data to an external service and changes the user's calendar state. The file does not include a confirmation prompt, user-visible log, or descriptive warning comment/docstring explaining that the operation will create an event remotely.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
This function issues a PATCH request to modify an existing calendar event, which is a state-changing operation on user data. There is no confirmation, user-facing log, or explanatory comment/docstring in this file to disclose that the user's calendar data will be altered.

Missing User Warnings

Medium
Confidence
84% confidence
Finding
This function creates a new contact in the user's Microsoft account, which writes personal data to an external service. The code does not provide a confirmation prompt, visible disclosure, or explanatory comment/docstring warning about this remote write operation.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
This function patches contact data in the user's account, which is a user-data modification operation. The file lacks any user-facing warning, confirmation, or explanatory comment/docstring indicating that contact records will be changed remotely.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This function uploads content to OneDrive using an authenticated PUT request, which writes user data to remote storage and can overwrite or create files. There is no confirmation prompt, user-facing log, or explanatory docstring/comment in this file to disclose that a remote file write is occurring.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The file contains many user-visible strings exclusively in Dutch, including errors, prompts, and status messages, but provides no opt-in, fallback, or documentation that the skill is intentionally Dutch-only. This creates a natural-language policy issue because the skill imposes a specific language on users without choice.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The code persists OAuth token material with saveTokens(data, discoveredAccountName) immediately after authentication, but this file provides no user-facing notice, consent, or storage-safety guarantees. Because the scopes include Mail, Calendar, Contacts, and Files with offline_access, stored refresh/access tokens can grant broad long-lived access if the local token store is readable by other users, malware, or backups.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The code reads a credentials file from `credentials/ms365.env` and copies its contents into `process.env`, which is a sensitive operation involving credentials. While there is error logging on failure, there is no confirmation prompt, user-facing notice, or explanatory comment warning that secrets from disk will be loaded into process environment state.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
OAuth token data is written to disk in plaintext under the user's home directory without any permission hardening or encryption. In a Microsoft 365 skill, these tokens can grant access to mail, calendar, contacts, and OneDrive data, so compromise of the local filesystem could expose sensitive account data or enable account actions.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
The natural-language instructions force a specific language for all users, and the file does not indicate that Dutch is optional or that the skill is intended only for a Dutch-speaking audience. Under the policy, locale constraints should be optional or clearly justified.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
This code presents user-facing status and error text in both Dutch and English, such as Dutch authentication errors alongside English success messages. That imposes a language choice on users without opt-in and matches the policy category for locale/language violations.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
The error message `Kon .env niet laden` is hard-coded in Dutch, and similar Dutch-only text appears elsewhere in the file. This is a natural-language locale choice imposed by the skill without opt-in or documented region-specific justification, which can violate language/locale policy expectations.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The message `Kon JSON niet lezen/parsen` is another user-visible Dutch-only string. Hard-coded locale-specific messaging across the file indicates the skill enforces a language choice rather than offering a neutral default or user preference.

Static analysis

No suspicious patterns detected.