Back to skill

Security audit

M365 Calendar (Graph)

Security checks for vulnerabilities and agentic risk

Overview

This Microsoft 365 calendar skill is mostly purpose-aligned, but it stores sensitive OAuth tokens with weak filesystem safeguards and has a profile path handling flaw that merits review before install.

Review this skill before installing on shared or multi-user machines. Use only trusted profile names, protect ~/.openclaw/secrets/m365-calendar with owner-only permissions, avoid raw-token import unless necessary, prefer explicit timezone arguments, and consider pinning dependencies with a lockfile before relying on it for business calendars.

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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/_lib.mjs:15
Finding
Profile Name Path Traversal Enables Filesystem Access Outside the Secrets Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/_lib.mjs:15-20` **Vulnerability Type**: Path traversal through an unvalidated profile identifier **Risk Level**: Medium ### Vulnerable Code ```js export function profilePaths(profile) { const dir = ensureSecretsDir(); return { cfgPath: path.join(dir, `${profile}.json`), cachePath: path.join(dir, `${profile}-token-cache.json`), }; } ``` The profile value is obtained directly from a command-line argument, including in `scripts/auth-devicecode.mjs:17` and `scripts/import-raw-token.mjs:14`: ```js const profile = mustGetArg('profile'); ``` The resulting paths are subsequently used for sensitive filesystem writes. For example, `scripts/import-raw-token.mjs:17-33` contains: ```js ensureSecretsDir(); const { cfgPath, cachePath } = profilePaths(profile); const raw = JSON.parse(fs.readFileSync(file, 'utf8')); if (!raw.access_token) throw new Error('Raw token file missing access_token'); // Store as a simple JSON cache format that _graph.mjs can consume when present. fs.writeFileSync(cachePath, JSON.stringify(raw, null, 2) + '\n', 'utf8'); writeJson(cfgPath, { clientId: raw.client_id || null, tenant: raw.tenant || 'consumers', email: raw.email || null, scopes: raw.scope ? String(raw.scope).split(' ') : [], authFlow: 'device_code_raw_import', createdAt: new Date().toISOString(), notes: 'Imported raw token JSON. Prefer MSAL cache when possible.', }); ``` ### Technical Analysis The `profile` argument is concatenated into two filenames without validation and passed to `path.join()`. Although `path.join()` normalizes a path, it does not enforce containment within the intended base directory. A profile containing traversal components such as `../../target` can therefore produce normalized paths outside: ```text ~/.openclaw/secrets/m365-calendar/ ``` The generated paths are used by several security-sensitive operations: - `scripts/auth-devicecode.mjs` writes profile configura ...[truncated 1982 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict profile identifiers to a conservative allowlist: ```js export function validateProfile(profile) { if (typeof profile !== 'string' || !/^[A-Za-z0-9_-]{1,64}$/.test(profile)) { throw new Error( 'Invalid profile name: use only letters, digits, underscores, and hyphens' ); } return profile; } ``` 2. Resolve the generated paths and enforce containment within the secrets directory: ```js export function profilePaths(profile) { validateProfile(profile); const dir = path.resolve(ensureSecretsDir()); const cfgPath = path.resolve(dir, `${profile}.json`); const cachePath = path.resolve(dir, `${profile}-token-cache.json`); const prefix = `${dir}${path.sep}`; if (!cfgPath.startsWith(prefix) || !cachePath.startsWith(prefix)) { throw new Error('Profile path escapes the secrets directory'); } return { cfgPath, cachePath }; } ``` 3. Apply validation centrally in `profilePaths()` so every caller receives the same protection. 4. Reject path separators, dot components, control characters, and empty profile names. 5. Add automated tests covering malicious inputs such as: ```text ../target ../../target . .. profile/name profile\name ``` 6. Consider refusing to overwrite existing non-profile files and use safe, atomic file creation where practical. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/_lib.mjs:9
Finding
OAuth Token and Configuration Files Are Created Without Restrictive Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/_lib.mjs:9-12, 27-28`; `scripts/auth-devicecode.mjs:50-54`; `scripts/import-raw-token.mjs:20-24`; `scripts/_graph.mjs:34-38` **Vulnerability Type**: Insecure storage permissions for authentication material **Risk Level**: Medium ### Vulnerable Code The secrets directory is created without an explicit restrictive mode in `scripts/_lib.mjs:9-12`: ```js export function ensureSecretsDir() { const dir = secretsDir(); fs.mkdirSync(dir, { recursive: true }); return dir; } ``` Generic JSON writes also omit a file mode in `scripts/_lib.mjs:27-28`: ```js export function writeJson(p, obj) { fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8'); } ``` The MSAL token cache is written without an explicit mode in `scripts/auth-devicecode.mjs:45-57`: ```js cache: { cachePlugin: { beforeCacheAccess: async (ctx) => { if (cache) ctx.tokenCache.deserialize(cache); }, afterCacheAccess: async (ctx) => { if (ctx.cacheHasChanged) { cache = ctx.tokenCache.serialize(); fs.writeFileSync(cachePath, cache, 'utf8'); } }, }, }, ``` Raw OAuth tokens are likewise written without an explicit mode in `scripts/import-raw-token.mjs:20-24`: ```js const raw = JSON.parse(fs.readFileSync(file, 'utf8')); if (!raw.access_token) throw new Error('Raw token file missing access_token'); // Store as a simple JSON cache format that _graph.mjs can consume when present. fs.writeFileSync(cachePath, JSON.stringify(raw, null, 2) + '\n', 'utf8'); ``` The normal token-refresh path also rewrites the cache without enforcing permissions in `scripts/_graph.mjs:31-40`: ```js cache: { cachePlugin: { beforeCacheAccess: async (ctx) => ctx.tokenCache.deserialize(cache), afterCacheAccess: async (ctx) => { if (ctx.cacheHasChanged) { cache = ctx.tokenCache.serialize(); fs.writeFileSync(cachePath, cache, 'utf8'); } }, }, }, ``` ### Technica ...[truncated 2398 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create and maintain the secrets directory with owner-only permissions: ```js export function ensureSecretsDir() { const dir = secretsDir(); fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); fs.chmodSync(dir, 0o700); return dir; } ``` 2. Write all configuration and token files with mode `0600`: ```js export function writeJson(p, obj) { fs.writeFileSync( p, JSON.stringify(obj, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 } ); fs.chmodSync(p, 0o600); } ``` 3. Centralize token-cache writes in a dedicated secure helper rather than calling `fs.writeFileSync()` directly: ```js export function writeSecretFile(p, content) { fs.writeFileSync(p, content, { encoding: 'utf8', mode: 0o600, }); fs.chmodSync(p, 0o600); } ``` 4. Use atomic writes to prevent partial cache corruption: - Create a temporary file in the same protected directory. - Set its mode to `0600`. - Write and flush the content. - Rename it atomically over the destination. - Avoid predictable temporary filenames. 5. Check and repair permissions on existing directories and token files during startup. 6. On platforms where POSIX modes are unavailable or insufficient, use platform-appropriate access-control lists so only the current user can read the files. 7. Avoid importing or retaining refresh tokens unless the user explicitly requests offline access, and clearly warn users that imported raw token files must also be protected or deleted securely after import. ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (6)

Credential Access

High
Category
Privilege Escalation
Content
- `email` (optional; informational)
  - `scopes` (calendar delegated scopes)
- `~/.openclaw/secrets/m365-calendar/<profile>-token-cache.json`
  - MSAL token cache (refresh token, access token, etc.)

## Why not workspace/secrets?
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Natural-Language Policy Violations

Low
Confidence
78% confidence
Finding
Multiple usage examples and recommended commands consistently force `--tz Europe/Vienna`, and the workflow does not indicate that the timezone should be chosen based on the user's preference or locale. This can violate language/locale policy by implicitly steering operation to a fixed locale without opt-in.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"type": "module",
  "description": "OpenClaw skill: Microsoft Graph calendar automation (M365 business + consumer).",
  "dependencies": {
    "@azure/msal-node": "^5.0.4"
  }
}
Confidence
91% confidence
Finding
The dependency is specified with a caret range (^5.0.4) rather than being pinned to an exact version, which makes builds non-reproducible and can result in different installed versions over time. In a security-sensitive skill that handles Microsoft Graph authentication flows, this increases supply-chain risk because a newly published semver-compatible release could introduce a vulnerability or malicious code without any manifest change.

Unverifiable Dependency: @azure/msal-node has 1 known advisory(ies) (CVE-2024-35255 (Azure Identity Libraries and Microsoft Authentication Library Elevation of Privi)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
84% confidence
Finding
The manifest references @azure/msal-node without an exact version pin, while the package has a known advisory in some releases, so it is not possible to verify from this file alone whether deployment will resolve to a patched or affected version. Because this skill automates Microsoft 365 calendar access via Microsoft Graph, any vulnerable authentication library could increase the risk of token handling, privilege, or account-compromise issues in a context that likely processes user credentials or delegated access.

Intent-Code Divergence

Low
Confidence
79% confidence
Finding
The statement at L03 suggests a strong safety property, but the rest of the document explicitly describes storing MSAL token cache files with refresh and access tokens under the user's home directory. While this does keep them out of git, it contradicts any implication that secrets are broadly protected or not persisted, which can mislead users about the actual handling of sensitive credentials.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The script defaults `tz` to `Europe/Vienna`, which imposes a specific locale-related behavior unless the user explicitly overrides it. This is a natural-language/policy-style locale constraint embedded in code behavior and is not documented here as justified or optional by default.