Back to skill

Security audit

MS Forms Auto

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it says, but it stores and reuses Microsoft 365 authentication material in ways users should review carefully before installing.

Install only if you are comfortable giving this skill local access to Microsoft 365 credentials, MFA-assisted sessions, calendar URLs, and organizational form data. Before use, add real gitignore rules, restrict file permissions, avoid passing MFA codes on the command line, remove or disable screenshot/HTML capture, and prefer OAuth or a secure credential store over plaintext passwords.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/submit-with-mfa.js:35
Finding
MFA Codes Are Exposed Through Command-Line Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/submit-with-mfa.js:35-43`, `scripts/mfa-login.js:101-104`, `scripts/submit-daily.js:27-32`, and `SKILL.md:128-141` **Vulnerability Type**: Sensitive authentication data exposed through process arguments **Risk Level**: Medium ### Complete Code Snippet ```javascript const args = process.argv.slice(2); let mfaCode = null; let targetDate = new Date().toISOString().split('T')[0]; let requireMFAAutoDetect = true; for (let i = 0; i < args.length; i++) { if (args[i] === '--code') { mfaCode = args[++i]; requireMFAAutoDetect = false; } if (args[i] === '--date') targetDate = args[++i]; if (args[i] === '--force-mfa') requireMFAAutoDetect = false; if (args[i] === '--no-mfa') requireMFAAutoDetect = true; } ``` The documented invocation is: ```bash node scripts/submit-with-mfa.js --code [MFA_CODE_HERE] ``` ### Technical Analysis MFA codes are supplied through the process command line. Command-line arguments are not an appropriate secret-transport mechanism because they may be exposed through: - Process inspection tools such as `ps` or `/proc/<pid>/cmdline` - Shell history - Cron configuration and execution logs - Process monitoring or endpoint telemetry - Terminal session recording - Wrapper scripts and automation logs Although an MFA code has a short lifetime, the workflow explicitly emphasizes immediate use. A local observer could therefore capture and reuse the code while it remains valid. The exposure is particularly significant when combined with separately compromised M365 credentials. ### Attack Path 1. The user or scheduled workflow runs the submission script with `--code 123456`. 2. A local user, monitoring agent, or log collector records the process arguments. 3. The attacker extracts the MFA code before it expires. 4. If the attacker also possesses the account password or has initiated a matching authentication attempt, the code can be used to complete that attempt. 5. The a ...[truncated 454 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove support for accepting MFA codes through `process.argv`. - Read the code from a hidden interactive prompt when a TTY is available. - For automation, accept the value through a protected pipe or inherited file descriptor. - If a temporary file is unavoidable, require owner-only permissions, read it once, and delete it immediately. - Do not embed MFA codes in cron commands, shell scripts, environment variables, or logs. - Update `SKILL.md` and testing documentation to remove all `--code XXXXXX` examples. - Prefer an officially supported authentication flow that does not require automating transient MFA secrets. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/submit-with-mfa.js:506
Finding
Reusable Browser Session Tokens Are Persisted Without Explicit Access-Control Enforcement<![CDATA[ ## Vulnerability Details **File Location**: `scripts/submit-with-mfa.js:506-520, 575-582`; also present in `scripts/mfa-login.js:64-70, 84-89`, `scripts/fill-form.js:154-155`, `scripts/submit-daily.js:272`, and `scripts/submit.js:193,379` **Vulnerability Type**: Insecure local storage of reusable authentication state **Risk Level**: High ### Complete Code Snippet ```javascript // Save updated auth state for future reuse const context = page.context(); await context.storageState({ path: AUTH_STATE }); console.log('💾 Auth state saved for potential reuse.'); return true; ``` The primary submission path performs another unprotected write: ```javascript // Save auth state after successful login (whether submit succeeded or not) for next time try { await context.storageState({ path: AUTH_STATE }); console.log('💾 Auth state saved.'); } catch (e) { console.log('⚠️ Could not save auth state:', e.message); } ``` The standalone MFA flow similarly writes the state: ```javascript // Save auth state const context = page.context(); await context.storageState({ path: AUTH_STATE }); console.log('✅ MFA completed! Auth state saved.'); console.log('URL:', page.url().substring(0, 100)); ``` ### Technical Analysis Playwright storage-state files may contain cookies, local-storage values, and other reusable authentication material. Possession of this file can allow another process to initialize a browser context as the authenticated user without repeating password or MFA verification. Unlike `credentials.json`, which is explicitly created with mode `0600`, the storage-state writes do not explicitly create or normalize the file with owner-only permissions. Effective access consequently depends on: - The process umask - Parent-directory permissions - Whether the file already existed with broader permissions - Shared-volume or workspace access controls - Backup and artifact-collection behavior Saving the state after login is functionally relevant to the Skill ...[truncated 1082 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the destination file securely before passing it to Playwright, with mode `0600` and verified ownership. - After every `storageState()` write, enforce and verify owner-only permissions with `chmodSync(AUTH_STATE, 0o600)`. - Ensure the `config` directory is accessible only to the intended operating-system account. - Refuse to consume a storage-state file owned by another user or readable by group/other accounts. - Store reusable authentication state in an operating-system credential store or encrypted secret store where practical. - Delete cached state when automation is disabled, authentication fails, or the account is changed. - Establish a short retention period and document revocation procedures. - Exclude the file from source control, backups, support bundles, and generic artifact collection. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/login-debug.js:18
Finding
Authenticated and Authentication-Flow Pages Are Saved as Unredacted Screenshots and HTML<![CDATA[ ## Vulnerability Details **File Location**: `scripts/login-debug.js:18-36, 65-68, 116-119, 151-154, 178-180`; also present in `scripts/submit-with-mfa.js:443-450, 503-512, 526-530`, `scripts/validate-submission.js:48-52`, and `scripts/fill-form.js:134-159` **Vulnerability Type**: Sensitive information exposure through persistent diagnostic artifacts **Risk Level**: High ### Complete Code Snippet ```javascript // Ensure screenshots directory exists if (!fs.existsSync(SCREENSHOT_DIR)) { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); } function saveScreenshot(page, filename) { const filepath = path.join(SCREENSHOT_DIR, filename); return page.screenshot({ path: filepath, fullPage: true }).then(() => { console.log(` 📸 Saved: ${filename}`); return filepath; }); } function saveHTML(page, filename) { const filepath = path.join(SCREENSHOT_DIR, filename); return page.content().then(html => { fs.writeFileSync(filepath, html.substring(0, 200000)); console.log(` 📄 Saved: ${filename}`); return filepath; }); } ``` The debug flow captures pages throughout authentication: ```javascript await saveScreenshot(page, '03-after-signin.png'); await saveHTML(page, '03-after-signin.html'); // Always capture final state await saveScreenshot(page, '04-final.png'); await saveHTML(page, '04-final.html'); ``` The submission flow also persists the completed page: ```javascript const auditDir = path.join(ROOT_DIR, 'audit-screenshots'); if (!fs.existsSync(auditDir)) fs.mkdirSync(auditDir, { recursive: true }); const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const auditBase = path.join(auditDir, 'submitted-' + targetDate + '-' + timestamp); await page.screenshot({ path: auditBase + '.png', fullPage: true }); const html = await page.content(); fs.writeFileSync(auditBase + '.html', html.substring(0, 200000)); ``` ### Technical Analysis The Skill saves full-page screenshots and large HTML fragments from l ...[truncated 1846 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Disable screenshot and HTML capture by default. - Require an explicit debug flag and informed user confirmation before collecting page content. - Never capture password-entry, MFA, consent, or session-establishment pages. - Redact email addresses, input values, challenge details, tokens, identifiers, and submitted responses. - Write diagnostic data to a private temporary directory with directory mode `0700` and file mode `0600`. - Automatically delete diagnostic artifacts at process exit or after a short configured retention period. - Store only minimal structured diagnostics, such as page state names and selector results, instead of complete HTML. - Prevent debug and audit directories from entering source control, backups, or generic support archives. - Add tests that verify diagnostic files cannot contain known credential, MFA, cookie, or form-response values. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/submit-with-mfa.js:554
Finding
Chromium Sandbox Is Disabled Across Authentication and Submission Workflows<![CDATA[ ## Vulnerability Details **File Location**: `scripts/submit-with-mfa.js:554-558`; also present in `scripts/check-auth.js:19`, `scripts/diagnose-login.js:22`, `scripts/fill-form.js:49`, `scripts/login-debug.js:49-52`, `scripts/mfa-login.js:27-30`, `scripts/submit-daily.js:266`, `scripts/submit.js:339-342`, `scripts/test-login.js:25`, and `scripts/validate-submission.js:32` **Vulnerability Type**: Browser process isolation disabled **Risk Level**: High ### Complete Code Snippet ```javascript const browser = await chromium.launch({ headless: false, args: ['--no-sandbox'] }); ``` Another execution path disables both sandbox mechanisms: ```javascript const browser = await chromium.launch({ headless: args.headed ? false : true, args: ['--no-sandbox', '--disable-setuid-sandbox'], }); ``` ### Technical Analysis The `--no-sandbox` Chromium flag disables an important isolation boundary between remote browser content and the privileges of the Node.js process. This is used throughout login, calendar-derived form submission, debugging, and validation scripts rather than being limited to a constrained compatibility mode. The browser processes handle remote Microsoft pages and potentially organization-controlled content. If a browser or renderer vulnerability is exploited, disabling the sandbox can make it easier for malicious content to access the process environment and local filesystem. This is particularly consequential because the process can access: - `config/credentials.json` - `config/storageState.json` - Token-bearing calendar configuration - Daily entry and audit files - Other files available to the operating-system user No browser exploit is included in the project, but removing this defense unnecessarily increases the impact of a compromised or malicious remote page. ### Attack Path 1. The user launches one of the Skill’s Playwright scripts. 2. Chromium starts with `--no-sandbox`. 3. The browser loads Microsoft pages or remote content ...[truncated 834 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove `--no-sandbox` and `--disable-setuid-sandbox` from normal execution. - Run Chromium as a non-root user with its supported sandbox enabled. - If container compatibility is required, configure the container with appropriate user namespaces, seccomp controls, and Chromium sandbox prerequisites. - Fail closed when sandbox initialization fails instead of silently disabling it. - If an exceptional no-sandbox mode must exist, require an explicit opt-in flag and display a strong warning. - Isolate that exceptional mode in a disposable container with: - No unrelated host filesystem mounts - Read-only project files where possible - A private temporary directory - Restricted network egress - No elevated Linux capabilities - No access to unnecessary secrets - Keep Playwright and its bundled Chromium version promptly patched. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:20
Finding
Documentation Claims Secret Files Are Git-Ignored but No Gitignore Rules Are Present<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:20-26, 110-115` and `scripts/setup-credentials.js:3-4` **Vulnerability Type**: Accidental secret disclosure caused by missing source-control exclusions **Risk Level**: High ### Complete Code Snippet ```markdown ### 2. Set Up Credentials (one-time) ```bash node scripts/setup-credentials.js ``` Creates `config/credentials.json` (gitignored) with M365 email/password. ``` The documentation also makes the following claims: ```markdown | File | Purpose | Gitignored? | |------|---------|-------------| | `config/credentials.json` | M365 email + password | ✅ | | `config/storageState.json` | Saved browser session (cookies) | ✅ | | `config/calendars.json` | Calendar URLs (contains auth tokens) | ✅ | | `config/calendars.json.example` | Template for calendar URLs | ❌ | | `config/form-values.json` | Legacy form value defaults | ❌ | | `daily-entries/` | Daily submission audit trail | ✅ | ``` The credential setup script repeats the assertion: ```javascript /** * Interactive M365 credential setup. * Saves email and password to config/credentials.json (gitignored). */ ``` However, the audited project tree contains no `.gitignore` file implementing these exclusions. ### Technical Analysis The Skill generates several sensitive files while telling users that they are protected from source-control inclusion. In the audited artifact, no `.gitignore` is present to enforce those claims. Sensitive generated content includes: - Plaintext M365 email and password in `config/credentials.json` - Reusable cookies and tokens in `config/storageState.json` - Token-bearing calendar URLs in `config/calendars.json` - Business activity information in `daily-entries/` - Screenshots and HTML generated by debugging and submission scripts - Form debug files in the current working directory The credentials file is locally protected with mode `0600`, but filesystem mode does not prevent its owner from accidentally adding it to ...[truncated 1166 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Add a project-root `.gitignore` containing at least: ```gitignore config/credentials.json config/storageState.json config/calendars.json daily-entries/ screenshots/ audit-screenshots/ form-debug.html form-debug.png diagnostic-*.png login-debug.* ``` - Add exclusions for all other generated HTML, PNG, trace, video, HAR, and authentication artifacts. - Add pre-commit secret scanning for passwords, cookies, calendar subscription tokens, and Microsoft authentication data. - Make setup scripts verify that sensitive paths are ignored with `git check-ignore` and warn or abort otherwise. - Update the documentation so protection claims match the files actually shipped. - If secrets were previously committed, rotate passwords, revoke sessions, regenerate calendar URLs, and purge repository history. - Use a secret manager or operating-system credential store rather than plaintext files where possible. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (59)

Credential Access

High
Category
Privilege Escalation
Content
node scripts/setup-credentials.js
```

Creates `config/credentials.json` (gitignored) with M365 email/password.

### 3. First Login with MFA
Confidence
95% confidence
Finding
The skill instructs creation of `config/credentials.json` containing M365 email and password, which is direct storage of primary credentials in a local file. Combined with browser automation and stored auth state, this materially increases the chance of credential theft and downstream compromise of corporate Microsoft 365 accounts.

Ae1

High
Category
analysis-evasion
Content
node scripts/calendar-fetch.js # Today (SGT)
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/calendar-fetch.js # Today (SGT)
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/calendar-fetch.js # Today (SGT)
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/calendar-fetch.js # Today (SGT)
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

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

Ae1

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

Ae1

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

Ae1

High
Category
analysis-evasion
Content
| `scripts/submit-with-mfa.js` | **Primary**: Combined MFA login + form submit in one session |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `scripts/submit-with-mfa.js` | **Primary**: Combined MFA login + form submit in one session |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `scripts/submit-with-mfa.js` | **Primary**: Combined MFA login + form submit in one session |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `scripts/submit-with-mfa.js` | **Primary**: Combined MFA login + form submit in one session |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `scripts/submit-with-mfa.js` | **Primary**: Combined MFA login + form submit in one session |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `scripts/submit-with-mfa.js` | **Primary**: Combined MFA login + form submit in one session |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `scripts/fill-form.js` | CLI form filler with `--date`, `--training`, etc. args |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
| File | Purpose | Gitignored? |
|------|---------|-------------|
| `config/credentials.json` | M365 email + password | ✅ |
| `config/storageState.json` | Saved browser session (cookies) | ✅ |
| `config/calendars.json` | Calendar URLs (contains auth tokens) | ✅ |
| `config/calendars.json.example` | Template for calendar URLs | ❌ |
Confidence
97% confidence
Finding
The configuration table documents multiple local files containing high-value secrets, including M365 credentials, session cookies, and calendar URLs with auth tokens. Even though they are marked gitignored, gitignore does not protect against local compromise, accidental copying, insecure backups, or exposure through logs and support bundles.

Credential Access

High
Category
Privilege Escalation
Content
### Prerequisites
- ✅ Skill installed at `~/.openclaw/skills/ms-forms-auto`
- ✅ `config/credentials.json` exists with valid M365 email/password
- ✅ `config/storageState.json` may exist (cached session) or be absent
- ✅ `daily-entries/2026-03-20.json` exists (backfilled entry for testing)
Confidence
92% confidence
Finding
This instruction explicitly relies on a local file containing valid M365 email/password credentials, normalizing direct credential storage for automated use. Even though the markdown is procedural rather than executable, encouraging use of plaintext or weakly protected credential files materially raises the risk of credential exposure, reuse, or compromise if the workspace, logs, or backups are accessed.

Credential Access

High
Category
Privilege Escalation
Content
---

### Test Case 5: Invalid Credentials
**Scenario:** Wrong email or password in `config/credentials.json`.

**Steps:**
```bash
Confidence
88% confidence
Finding
Referencing invalid email/password values in config/credentials.json reinforces a pattern where live account credentials are expected to be edited and tested from a file. This is less severe than the prerequisite itself, but it still promotes insecure credential handling practices and may lead users to manipulate real secrets in local files during troubleshooting.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
- To force fresh login for testing, delete `config/storageState.json` before Test Cases 1-3.

```bash
rm /home/ubuntu/.openclaw/workspace/skills/ms-forms-auto/config/storageState.json
```

---
Confidence
85% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Credential Access

High
Category
Privilege Escalation
Content
const ROOT_DIR = path.resolve(path.join(__dirname, '..'));
const CONFIG_DIR = path.join(ROOT_DIR, 'config');
const CREDS_FILE = path.join(CONFIG_DIR, 'credentials.json');
const FORM_URL = 'https://forms.cloud.microsoft/r/LsxLaEv13i';

async function diagnose() {
Confidence
84% confidence
Finding
Referencing a dedicated `credentials.json` file in a script that automates and diagnoses a Microsoft login flow is a strong credential-access indicator. In skill context, this is more dangerous because the script targets an authentication surface and combines secret-file access with browser automation, a pattern commonly associated with credential harvesting or misuse even if this specific file does not yet submit the credentials.

Credential Access

High
Category
Privilege Escalation
Content
const ROOT_DIR = path.join(__dirname, '..');
const CONFIG_DIR = path.join(ROOT_DIR, 'config');
const CREDS_FILE = path.join(CONFIG_DIR, 'credentials.json');
const AUTH_STATE = path.join(CONFIG_DIR, 'storageState.json');
const SCREENSHOT_DIR = path.join(ROOT_DIR, 'screenshots');
const FORM_URL = 'https://forms.cloud.microsoft/r/LsxLaEv13i';
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

High
Confidence
95% confidence
Finding
The script saves full-page screenshots and raw HTML from authentication flows to disk in a local screenshots directory. Login pages, MFA prompts, error pages, and post-authenticated pages can contain usernames, PII, session-related data, CSRF tokens, or authenticated content, making this a real sensitive-data exposure risk if files are accessed by other users, synced, or committed to source control.

Credential Access

High
Category
Privilege Escalation
Content
const ROOT_DIR = path.resolve(path.join(__dirname, '..'));
const CONFIG_DIR = path.join(ROOT_DIR, 'config');
const CREDS_FILE = path.join(CONFIG_DIR, 'credentials.json');
const AUTH_STATE = path.join(CONFIG_DIR, 'storageState.json');
const FORM_URL = 'https://forms.cloud.microsoft/r/LsxLaEv13i';
Confidence
91% confidence
Finding
The explicit dependency on a local credentials.json file indicates the skill is designed to obtain account credentials from disk for automated login. In this context, that is a real credential-access pattern because it centralizes sensitive secrets in a predictable file path and combines them with browser automation to access a third-party account.

Credential Access

High
Category
Privilege Escalation
Content
#!/usr/bin/env node
/**
 * Interactive M365 credential setup.
 * Saves email and password to config/credentials.json (gitignored).
 */

const fs = require('fs');
Confidence
82% confidence
Finding
This script is explicitly designed to collect and persist a Microsoft 365 email and password in a local JSON file. Storing a primary account password in plaintext on disk creates a credential theft risk from local compromise, backups, malware, or accidental exposure, even if the file is gitignored and mode 600 is requested.

Credential Access

High
Category
Privilege Escalation
Content
const readline = require('readline');

const CONFIG_DIR = path.join(__dirname, '..', 'config');
const CREDENTIALS_PATH = path.join(CONFIG_DIR, 'credentials.json');

function prompt(question, hidden = false) {
  return new Promise((resolve) => {
Confidence
87% confidence
Finding
The dedicated path to credentials.json indicates intentional local secret storage for M365 credentials. Centralizing credentials in a predictable file path increases the chance that other local processes, malware, or users on a shared system can target and harvest the file.

Static analysis

No suspicious patterns detected.