Back to skill

Security audit

Miro Management

Security checks for vulnerabilities and agentic risk

Overview

This Miro skill is mostly purpose-aligned, but it handles OAuth tokens and live board-changing API calls with too little protection for a normal install.

Install only if you are comfortable giving this skill access to your Miro workspace and using it carefully on live boards. Prefer a least-privilege Miro app, avoid direct bearer-token entry in shared shells, store token files outside shared or synced folders, rotate any token that appears in logs, and review raw/delete/webhook commands before running them.

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

Error
Location
scripts/miro_api.py:88
Finding

OAuth credentials and tokens are transmitted in URL query parameters

Content
View full analysis
Dict[str, Any]: url = TOKEN_URL + "?" + urllib.parse.urlencode(params) req = urllib.request.Request(url, method="POST") try: with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode("utf-8", errors="replace")) except urllib.error.HTTPError as e: body = e.read().decode("utf-8", errors="replace") try: parsed = json.loads(body) if body else {"error": str(e)} except json.JSONDecodeError: parsed = {"error": body or str(e)} print_json(parsed) sys.exit(1) def exchange_code(client_id: str, client_secret: str, redirect_uri: str, code: str) -> Dict[str, Any]: return token_request({ "grant_type": "authorization_code", "client_id": client_id, "client_secret": client_secret, "code": code, "redirect_uri": redirect_uri, }) def refresh_token(client_id: str, client_secret: str, refresh_token_value: str) -> Dict[str, Any]: return token_request({ "grant_type": "refresh_token", "client_id": client_id, "client_secret": client_secret, "refresh_token": refresh_token_value, }) ``` ### Technical Analysis The token endpoint request uses the POST method but places all OAuth parameters in the URL query string rather than in the request body. Consequently, the following sensitive values may appear in the complete URL: - Miro OAuth client secret - Authorization code - Refresh token - Client identifier and redirect URI The destination is the official HTTPS Miro token endpoint, so sending these credentials to that service is necessary for the declared OAuth functionality. TLS also protects the request while it i ...[truncated 1718 chars]
Remediation
View remediation
Dict[str, Any]: data = urllib.parse.urlencode(params).encode("utf-8") req = urllib.request.Request( TOKEN_URL, data=data, headers={ "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", }, method="POST", ) with urllib.request.urlopen(req) as resp: return json.loads(resp.read().decode("utf-8", errors="replace")) ``` 2. Ensure request URLs, headers, and bodies containing credentials are not emitted to logs. 3. Configure HTTP diagnostics and exception telemetry to redact `client_secret`, `code`, `access_token`, and `refresh_token`. 4. Rotate any client secret or refresh token that may already have been captured in URL logs. 5. Add automated tests asserting that the token endpoint URL has no sensitive query parameters. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/miro_api.py:218
Finding

OAuth access and refresh tokens are printed to standard output

Content
View full analysis
None: client_id = resolve_env_or_arg(args, "client_id", "MIRO_CLIENT_ID") client_secret = resolve_env_or_arg(args, "client_secret", "MIRO_CLIENT_SECRET") redirect_uri = resolve_env_or_arg(args, "redirect_uri", "MIRO_REDIRECT_URI") data = exchange_code(client_id, client_secret, redirect_uri, args.code) if args.token_file: save_tokens(args.token_file, data) maybe_output(args, data) def cmd_refresh_token(args: argparse.Namespace) -> None: client_id = resolve_env_or_arg(args, "client_id", "MIRO_CLIENT_ID") client_secret = resolve_env_or_arg(args, "client_secret", "MIRO_CLIENT_SECRET") tokens = load_tokens(args.token_file) data = refresh_token(client_id, client_secret, tokens["refresh_token"]) save_tokens(args.token_file, data) maybe_output(args, data) ``` The callback flow also retains and prints the complete token response: ```python token_data = exchange_code(client_id, client_secret, redirect_uri, code) save_tokens(token_file, token_data) result["done"] = True result["token_data"] = token_data ``` ```python print_json(result) ``` `maybe_output()` always prints the supplied value: ```python def maybe_output(args: argparse.Namespace, data: Any) -> None: print_json(data) output = getattr(args, "output", None) if output: write_json(output, data) ``` ### Technical Analysis Miro token endpoint responses can contain both an access token and a refresh token. The exchange and refresh commands pass the complete response to `maybe_output()`, which unconditionally writes it to standard output. The OAuth callback helper similarly inserts the full token response into `result` and prints that result after authori ...[truncated 1599 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/miro_api.py:25
Finding

OAuth token files are created without restrictive file permissions

Content
View full analysis
None: p = Path(path) ensure_parent(p) p.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") ``` ```python def save_tokens(token_file: str, data: Dict[str, Any]) -> None: write_json(token_file, data) ``` ### Technical Analysis OAuth token responses are written through the same generic JSON writer used for non-sensitive output. The code does not explicitly restrict the token file to the current user. For a newly created file, effective permissions depend on the process umask and operating-system defaults. On a system with permissive defaults, the file may be readable by a local group or other users. When an existing file is overwritten, any previously unsafe permissions are retained. The code also creates parent directories without explicitly restricting their mode. Even if a token file is not directly readable, permissive directory configuration can increase exposure through listing, replacement, or configuration mistakes. ### Attack Path 1. A user selects a token path and completes OAuth or refreshes a token. 2. `save_tokens()` writes the access and refresh tokens through `Path.write_text()`. 3. The resulting file inherits permissions from the environment or preserves insecure existing permissions. 4. Another local user or process with filesystem access reads the token file. 5. The attacker uses the access token directly or exchanges the refresh token for a new access token. 6. The attacker performs Miro API actions within the integration’s granted scopes. ### Impact Assessment Exploitation requires local filesystem access or access through another service running under a principal that can read the file. If successful, it exposes the same Miro ...[truncated 361 chars]
Remediation
View remediation
None: path = Path(token_file) path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) os.chmod(path.parent, 0o700) payload = json.dumps(data, indent=2, ensure_ascii=False) + "\n" fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) try: with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(payload) finally: os.chmod(path, 0o600) ``` Platform-specific error handling should be added where POSIX permission modes are unavailable. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/miro_api.py:80
Finding

OAuth callback is not bound to the initiating session with a state parameter

Content
View full analysis
str: params = urllib.parse.urlencode({ "response_type": "code", "client_id": client_id, "redirect_uri": redirect_uri, }) return f"{AUTH_BASE}?{params}" ``` The callback accepts an authorization code based only on the expected URL path: ```python parsed = urllib.parse.urlparse(redirect_uri) expected_path = parsed.path or "/" token_file = args.token_file result: Dict[str, Any] = {"done": False} class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): url = urllib.parse.urlparse(self.path) if url.path != expected_path: self.send_response(404) self.end_headers() self.wfile.write(b"Not found") return qs = urllib.parse.parse_qs(url.query) if "error" in qs: result["done"] = True result["error"] = qs.get("error", [""])[0] self.send_response(200) self.end_headers() self.wfile.write(b"Miro authorization returned an error. You can close this window.") return code = qs.get("code", [None])[0] if not code: self.send_response(400) self.end_headers() self.wfile.write(b"Missing code") return token_data = exchange_code(client_id, client_secret, redirect_uri, code) save_tokens(token_file, token_data) result["done"] = True result["token_data"] = token_data self.send_response(200) self.end_headers() self.wfile.write(b"Miro authorization complete. Tokens saved. You can close this window.") ``` ### Technical Analysis OA ...[truncated 2168 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (21)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 3)May include surrounding context.

md
---
name: miro-management
description: Manage Miro through the Miro REST API using OAuth 2.0, saved token files, or direct access tokens. Use when the user wants to connect a personal or local Miro integration, complete OAuth authorization, refresh tokens, list boards, inspect board items or members, create or delete common board content such as sticky notes, text, shapes, cards, and connectors, manage webhooks, export board data, preview write payloads safely, or send raw REST API calls in a reusable local workflow.
---

# Miro Management

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 20)May include surrounding context.

md
---
name: miro-management
description: Manage Miro through the Miro REST API using OAuth 2.0, saved token files, or direct access tokens. Use when the user wants to connect a personal or local Miro integration, complete OAuth authorization, refresh tokens, list boards, inspect board items or members, create or delete common board content such as sticky notes, text, shapes, cards, and connectors, manage webhooks, export board data, preview write payloads safely, or send raw REST API calls in a reusable local workflow.
---

# Miro Management

Credential Access

High
Category
Privilege Escalation
Confidence
78% confidence
Finding

The skill instructs users to place a live Miro access token directly into an environment variable for immediate use. While common operationally, this still normalizes direct bearer-token handling and, combined with the skill's raw API and file-based token workflows, can expose credentials through shell history, process inspection, logs, or accidental reuse without stronger storage controls.

Content

Scanner excerpt · SKILL.md (reported line 50)May include surrounding context.

Direct token mode

If the user already has a working Miro access token, skip OAuth and use either:

powershell
$env:MIRO_ACCESS_TOKEN = '...'

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/miro-oauth-notes.md (reported line 45)May include surrounding context.

md
Miro docs say:

- access token valid for about 60 minutes
- refresh token valid for about 60 days
- refresh returns a new access token and new refresh token

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/miro-oauth-notes.md (reported line 47)May include surrounding context.

md
Miro docs say:

- access token valid for about 60 minutes
- refresh token valid for about 60 days
- refresh returns a new access token and new refresh token

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/miro-request-examples.md (reported line 103)May include surrounding context.

python scripts/miro_api.py raw POST /boards/{board_id}/sticky_notes --body-file sticky.json

text

## Access token shortcuts

Use either a saved token file or a direct token:

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/miro_api.py (reported line 70)May include surrounding context.

python
token = tokens.get("access_token")
        if token:
            return token
    print("Missing access token. Pass --access-token, set MIRO_ACCESS_TOKEN, or use --token-file.", file=sys.stderr)
    sys.exit(2)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/miro_api.py (reported line 536)May include surrounding context.

python
token = tokens.get("access_token")
        if token:
            return token
    print("Missing access token. Pass --access-token, set MIRO_ACCESS_TOKEN, or use --token-file.", file=sys.stderr)
    sys.exit(2)

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/miro_api.py (reported line 583)May include surrounding context.

python
token = tokens.get("access_token")
        if token:
            return token
    print("Missing access token. Pass --access-token, set MIRO_ACCESS_TOKEN, or use --token-file.", file=sys.stderr)
    sys.exit(2)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill describes capabilities that require environment variable access, local file reads/writes, and outbound network access, but it does not declare an explicit tool scope or permissions boundary. This increases the risk of over-broad execution in hosts that rely on manifest-declared permissions, making sensitive token handling and external API mutations less constrained than they should be.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The examples prominently demonstrate board-modifying operations such as creating and updating content, but do not warn that these commands act on live Miro boards. In an automation skill intended for reusable local workflows, that omission increases the chance of accidental destructive or unintended changes to real user data.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The raw request example shows how to send arbitrary API calls without warning that it can perform unrestricted write actions against any permitted endpoint. Because this bypasses higher-level guardrails and normalizes direct POST usage, users may unintentionally issue destructive or overbroad requests to live boards.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The access token shortcut instructs users to place a bearer token in an environment variable without warning that the token is a sensitive credential. In practice, such tokens may be exposed through shell history, screenshots, shared terminals, logs, or inherited environments, enabling unauthorized access to Miro resources.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · references/miro-oauth-notes.md (reported line 6)May include surrounding context.

md
from typing import Any, Dict, Optional

AUTH_BASE = "https://miro.com/oauth/authorize"
TOKEN_URL = "https://api.miro.com/v1/oauth/token"
API_BASE = "https://api.miro.com/v2"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · references/miro-oauth-notes.md (reported line 7)May include surrounding context.

md
from typing import Any, Dict, Optional

AUTH_BASE = "https://miro.com/oauth/authorize"
TOKEN_URL = "https://api.miro.com/v1/oauth/token"
API_BASE = "https://api.miro.com/v2"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/miro_api.py (reported line 16)May include surrounding context.

python
from typing import Any, Dict, Optional

AUTH_BASE = "https://miro.com/oauth/authorize"
TOKEN_URL = "https://api.miro.com/v1/oauth/token"
API_BASE = "https://api.miro.com/v2"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/miro_api.py (reported line 17)May include surrounding context.

python
from typing import Any, Dict, Optional

AUTH_BASE = "https://miro.com/oauth/authorize"
TOKEN_URL = "https://api.miro.com/v1/oauth/token"
API_BASE = "https://api.miro.com/v2"

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

OAuth access and refresh tokens are saved to a user-specified file in plaintext with no permission hardening or warning about the sensitivity of the stored credentials. On multi-user systems, in shared workspaces, or when the file is later committed/backed up, these tokens could be stolen and used to access or modify the victim's Miro data.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The export functionality writes board contents, including item text and metadata, to local markdown, CSV, or JSON files without any warning that the output may contain sensitive collaboration data. This increases the chance of accidental disclosure through shared folders, source control, logs, or insecure workstation storage.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The raw command allows arbitrary authenticated API requests, including destructive operations such as DELETE or state-changing POST/PATCH/PUT, with no confirmation barrier or warning. In an agent skill context, this is more dangerous because a user or upstream workflow may invoke broad remote actions on live Miro resources with little friction or review.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/miro_api.py (reported line 41)May include surrounding context.

python
def resolve_env_or_arg(args: argparse.Namespace, arg_name: str, env_name: str, required: bool = True) -> Optional[str]:
    value = getattr(args, arg_name, None) or os.environ.get(env_name)
    if required and not value:
        print(f"Missing {arg_name}. Pass --{arg_name.replace('_','-')} or set {env_name}.", file=sys.stderr)
        sys.exit(2)

Static analysis

No suspicious patterns detected.