Back to skill

Security audit

Miro board

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Miro automation, but it can modify and delete board content and ships prior state that could drive destructive undo behavior.

Review before installing. Use a least-privilege Miro token for only the intended board, remove or reset _out/.state.json before use, and require the agent to show the target JSON path, sessionKey, board ID, and command before running apply or undo.

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
miro-push.mjs:372
Finding
Destructive replacement occurs before validation of the new diagram## Vulnerability Details **File Location**: `miro-push.mjs`, lines 372-405 **Vulnerability Type**: Destructive operation ordering and insufficient input validation **Risk Level**: Medium ### Vulnerable Code ```js async function undo(sessionKey) { const state = loadState(); const sess = state.sessions?.[sessionKey]; if (!sess?.lastRun) { console.log(`Nothing to undo for sessionKey=${sessionKey}`); return; } for (const id of sess.lastRun.connectorIds ?? []) await deleteConnector(id).catch(() => {}); for (const id of sess.lastRun.stickyIds ?? []) await deleteSticky(id).catch(() => {}); for (const id of sess.lastRun.frameIds ?? []) await deleteFrame(id).catch(() => {}); state.sessions[sessionKey].lastRun = null; saveState(state); console.log(`Undone sessionKey=${sessionKey}`); } async function apply(jsonPath) { const doc = JSON.parse(fs.readFileSync(jsonPath, "utf-8")); const meta = doc.meta ?? {}; const sessionKey = String(meta.sessionKey ?? "").trim(); if (!sessionKey) throw new Error("meta.sessionKey is required"); const state = loadState(); state.sessions = state.sessions ?? {}; state.sessions[sessionKey] = state.sessions[sessionKey] ?? { lastRun: null }; // Idempotent run if (state.sessions[sessionKey].lastRun) await undo(sessionKey); const frames = Array.isArray(doc.frames) ? doc.frames : []; const stickies = Array.isArray(doc.stickies) ? doc.stickies : []; const connectors = Array.isArray(doc.connectors) ? doc.connectors : []; ``` ### Technical Analysis The `apply` operation validates only that `meta.sessionKey` is nonempty before invoking `undo`. The previous Miro objects are therefore deleted before the replacement document's frames, stickies, connectors, identifiers, dimensions, and API feasibility are validated. If any subsequent Miro creation request fails, the script's rollback logic can delete only the newly created objec ...[truncated 1511 chars]
Remediation
## Remediation Suggestions 1. Define and enforce a strict JSON schema before performing any network-side deletion: - Require unique, nonempty frame and sticky IDs. - Validate all connector endpoints. - Validate frame references used by stickies. - Enforce finite numeric coordinates and safe dimension limits. - Enforce maximum counts and string lengths. 2. Create and verify the replacement run before deleting the previous run whenever Miro semantics permit it. 3. If delete-first replacement is unavoidable, retain the complete prior source document and implement a tested restoration transaction. 4. Do not silently suppress deletion errors. Collect failures, retain affected IDs in state, and report that the undo was incomplete. 5. Clear `lastRun` only after every required deletion succeeds. Otherwise preserve unresolved IDs for retry. 6. Consider binding each state session to the configured board ID so that state cannot accidentally be reused against another board.

T09 · Insecure Skill Coding Practices

Note
Location
_out/.state.json:2
Finding
Distributed state file exposes operational Miro identifiers used by destructive undo logic## Vulnerability Details **File Location**: `_out/.state.json`, lines 2-109 **Vulnerability Type**: Exposure of operational metadata and unsafe distribution of mutable state **Risk Level**: Low ### Vulnerable Data ```json { "sessions": { "workshop-board-3-categorie": { "lastRun": { "frameIds": [], "stickyIds": [ "3458764659364173560", "3458764659364173564", "3458764659364173570" ], "connectorIds": [ "3458764659364173640", "3458764659364173647", "3458764659364173648" ] } }, "product-a-b-diagram-no-titles": { "lastRun": { "frameIds": [ "3458764659410912745", "3458764659410912749" ], "stickyIds": [ "3458764659410912750", "3458764659410912753" ], "connectorIds": [ "3458764659411051983" ] } } } } ``` ### Technical Analysis The packaged project contains persistent state populated with prior session names and numerous real-looking Miro frame, sticky-note, and connector identifiers. These identifiers are not authentication credentials and cannot provide access by themselves. However, they expose operational metadata and are directly consumed by the script's destructive `undo` operation. Shipping mutable state also creates a state-integrity risk. A recipient who has credentials for the corresponding board can invoke `undo` with one of the disclosed session keys, causing the script to attempt deletion of all recorded objects. The state file contains no board binding or integrity protection to confirm that it belongs to the currently configured `MIRO_BOARD_ID`. ### Attack Path 1. A recipient obtains the distributed Skill package. 2. The recipient reads `_out/.state.json` and discovers session names and Miro object IDs. 3. If the recipie ...[truncated 706 chars]
Remediation
## Remediation Suggestions 1. Remove populated `_out/.state.json` data before publishing or distributing the Skill. 2. Add `_out/.state.json` and generated `miro-ready-*.json` files to version-control and packaging exclusions. 3. Ship only an empty state template such as `{ "sessions": {} }`. 4. Restrict state-file permissions to the account running the Skill. 5. Store the associated board ID with each session and reject undo operations when it does not match the configured board. 6. Consider adding state integrity protection where untrusted local users can modify the project directory.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (16)

Credential Access

High
Category
Privilege Escalation
Content
1) REQUIREMENTS
- OpenClaw installed and working
- Node.js with ESM support (Node 18+ recommended)
- A MIRO Developer App with an Access Token
- Access to the target MIRO board
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
1) REQUIREMENTS
- OpenClaw installed and working
- Node.js with ESM support (Node 18+ recommended)
- A MIRO Developer App with an Access Token
- Access to the target MIRO board
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
- Access to the target MIRO board


2) MIRO SETUP (ACCESS TOKEN)
You need a MIRO access token. Do NOT use cookies or browser session tokens.

Steps:
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
2) MIRO SETUP (ACCESS TOKEN)
You need a MIRO access token. Do NOT use cookies or browser session tokens.

Steps:
1. Create a MIRO Developer App (MIRO Developers portal)
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
2) MIRO SETUP (ACCESS TOKEN)
You need a MIRO access token. Do NOT use cookies or browser session tokens.

Steps:
1. Create a MIRO Developer App (MIRO Developers portal)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The example invocation phrase, "Transfer this into MIRO with containers + clean arrows," is broad and resembles an ordinary user request rather than a clearly scoped skill trigger. In agent environments, this can cause accidental or unintended activation of the skill on unrelated content, leading to unreviewed data being transformed and pushed into an external Miro board.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill explicitly instructs the agent to save a file locally and execute a local Node.js script that modifies an external Miro board, but it does not require any user-facing consent, warning, or confirmation before performing those side effects. In an agent setting, this creates a real risk of unexpected filesystem writes and outbound state-changing actions against third-party services, especially if the user did not intend immediate execution.

External Transmission

Medium
Category
Data Exfiltration
Content
}

// --- delete helpers ---
const deleteFrame = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/frames/${id}`);
const deleteSticky = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/sticky_notes/${id}`);
const deleteConnector = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/connectors/${id}`);
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

// --- delete helpers ---
const deleteFrame = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/frames/${id}`);
const deleteSticky = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/sticky_notes/${id}`);
const deleteConnector = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/connectors/${id}`);
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

// --- delete helpers ---
const deleteFrame = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/frames/${id}`);
const deleteSticky = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/sticky_notes/${id}`);
const deleteConnector = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/connectors/${id}`);
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

// --- delete helpers ---
const deleteFrame = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/frames/${id}`);
const deleteSticky = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/sticky_notes/${id}`);
const deleteConnector = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/connectors/${id}`);
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

// --- delete helpers ---
const deleteFrame = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/frames/${id}`);
const deleteSticky = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/sticky_notes/${id}`);
const deleteConnector = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/connectors/${id}`);
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

// --- delete helpers ---
const deleteFrame = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/frames/${id}`);
const deleteSticky = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/sticky_notes/${id}`);
const deleteConnector = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/connectors/${id}`);
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

// --- delete helpers ---
const deleteFrame = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/frames/${id}`);
const deleteSticky = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/sticky_notes/${id}`);
const deleteConnector = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/connectors/${id}`);
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

// --- delete helpers ---
const deleteFrame = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/frames/${id}`);
const deleteSticky = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/sticky_notes/${id}`);
const deleteConnector = (id) => api("DELETE", `https://api.miro.com/v2/boards/${boardId}/connectors/${id}`);
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
The JSON schema constrains `meta.language` to `it|de|en`, but the document does not say that the user can choose the language or that the restriction is justified by a region-specific purpose. This can violate language/locale policy by silently forcing a limited locale set.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
miro-push.mjs:21