Back to skill

Security audit

Mai

Security checks for vulnerabilities and agentic risk

Overview

Mai is a coherent shopping and marketplace skill, but its hosted registry has authorization and deployment weaknesses that could expose or corrupt order and marketplace data.

Install only if you need Mai's commerce workflows and understand that it stores local and registry marketplace data. For hosted registry use, fix /sync/push authorization first, bind the service to localhost or place it strictly behind HTTPS, protect and rotate API keys, and treat the demo payment provider as non-real money movement.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/mai_registry.py:228
Finding

Merchant Snapshot Push Permits Cross-Tenant Order and Marketplace Data Tampering

Content
View full analysis

Vulnerability Details

File Location: scripts/mai_registry.py:228-235, 569-582
Vulnerability Type: Incomplete object-level authorization in snapshot ingestion
Risk Level: High

Vulnerable Code

The snapshot merge function accepts and persists orders, messages, reviews, and inventory events without validating their ownership:

python
for order_id, order in snapshot.get("orders", {}).items():
    registry["orders"][order_id] = order
    counts["orders"] += 1
counts["messages"] = dedupe_list_append(registry["messages"], snapshot.get("messages", []))
counts["reviews"] = dedupe_list_append(registry["reviews"], snapshot.get("reviews", []))
counts["inventory_events"] = dedupe_list_append(
    registry["inventory_events"],
    snapshot.get("inventory_events", []),
)

The /sync/push authorization logic validates only the merchant and product portions of the payload before passing the complete payload to merge_snapshot():

python
if parsed.path == "/sync/push":
    principal = self.require_authenticated(principal)
    if principal["role"] == "merchant":
        allowed = principal.get("merchant_id", "")
        merchants = set(payload.get("merchants", {}).keys())
        product_merchants = {
            product.get("merchant_id", "")
            for product in payload.get("products", {}).values()
        }
        if merchants - {allowed} or product_merchants - {allowed}:
            raise PermissionError(f"merchant access required: {allowed}")
    elif principal["role"] != "admin":
        raise PermissionError("merchant or admin role required")
    counts = merge_snapshot(registry, payload)

Technical Analysis

The registry applies tenant authorization to merchants and products, but not to the other collections accepted by the same endpoint. Consequently, any authenticated merchant can provide empty or correctly scoped merchant and product maps while addi ...[truncated 1944 chars]

Remediation
View remediation

Remediation Suggestions

  1. Make merchant snapshot pushes catalog-only at the server boundary. Reject merchant payloads containing non-empty orders, messages, inventory_events, synchronization events, or other privileged collections.
  2. Do not pass the original unrestricted payload to merge_snapshot(). Construct an allowlisted payload containing only validated merchant and product records.
  3. If review publication is allowed, validate that each review is associated with an authorized and verified transaction rather than accepting arbitrary snapshots.
  4. Restrict full-state migration to administrators through a separate endpoint and require explicit migration authorization.
  5. Validate the merchant_id of every nested record, including orders, messages, reviews, and inventory events.
  6. Prevent snapshot ingestion from overwriting existing orders. Use dedicated, state-transition-aware order endpoints with optimistic concurrency or immutable event records.
  7. Add negative tests proving that a merchant cannot create or overwrite objects associated with another merchant or buyer.
  8. Record an audit event for every administrative migration or attempted cross-tenant write.

T09 · Insecure Skill Coding Practices

Warning
Location
docker-compose.yml:11
Finding

Default Container Deployment Exposes Bearer-Authenticated Registry over Plain HTTP

Content
View full analysis

Vulnerability Details

File Location: docker-compose.yml:11-18; related configuration in Dockerfile:17 and credential transmission in scripts/mai.py:188-195
Vulnerability Type: Insecure transport and unsafe network exposure defaults
Risk Level: Medium

Vulnerable Code

The Compose configuration binds the HTTP registry to every container interface and publishes it on the host:

yaml
    - --host
    - 0.0.0.0
    - --port
    - "8765"
    - --rate-limit-per-minute
    - "${MAI_RATE_LIMIT_PER_MINUTE:-60}"
  ports:
    - "${MAI_REGISTRY_PORT:-8765}:8765"

The image has the same all-interface HTTP default:

dockerfile
CMD ["python3", "scripts/mai_registry.py", "serve", "--data", "/data/mai-registry.json", "--host", "0.0.0.0", "--port", "8765", "--rate-limit-per-minute", "60"]

Registry credentials are transmitted as bearer tokens:

python
headers = {"Accept": "application/json"}
if api_key:
    headers["Authorization"] = f"Bearer {api_key}"
if payload is not None:
    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    headers["Content-Type"] = "application/json; charset=utf-8"
request = Request(url, data=data, headers=headers, method=method)

Technical Analysis

The registry server itself provides HTTP rather than TLS. The supplied container deployment publishes that service on all host interfaces by default. Although the documentation advises operators to deploy an HTTPS reverse proxy, the provided deployment does not enforce this requirement or restrict the published port to loopback.

If the Compose stack is started without additional firewall or reverse-proxy controls, clients may communicate directly with port 8765 over plaintext HTTP. Bearer API keys have replay value: anyone who obtains a token can use the privileges assigned to that key until it is disabled.

The CLI correctly rejects non-HTTPS remote registry URLs, but this does ...[truncated 1421 chars]

Remediation
View remediation

Remediation Suggestions

  1. Bind the published port to loopback by default:

    yaml
    ports:
      - "127.0.0.1:${MAI_REGISTRY_PORT:-8765}:8765"
    
  2. Prefer not publishing the application port at all. Place the registry on an internal container network and expose only an HTTPS reverse proxy.

  3. Provide an HTTPS-by-default production deployment profile with certificate validation and secure redirect behavior.

  4. Refuse non-loopback server binding unless the operator explicitly confirms secure proxy mode.

  5. Configure network policy or firewall rules so clients cannot bypass the TLS proxy and contact port 8765 directly.

  6. Add short-lived or rotatable credentials and document immediate token revocation procedures.

  7. Avoid placing real API keys in command-line history; load them from a secret manager or protected credential file.

  8. Add deployment verification that fails when a publicly reachable registry endpoint does not use HTTPS.

Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (16)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared purpose is a commerce matchmaking agent, but the documented behavior includes installer operations, symlink creation, and possible overwrite/delete behavior during installation. This mismatch is dangerous because users may authorize a shopping skill expecting business logic while the package also performs privileged local environment modification that can alter host agent directories or replace existing paths.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared purpose is a commerce matchmaking agent, but the documented behavior includes installer operations, symlink creation, and possible overwrite/delete behavior during installation. This mismatch is dangerous because users may authorize a shopping skill expecting business logic while the package also performs privileged local environment modification that can alter host agent directories or replace existing paths.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- Confirm `SKILL.md` has no scaffold placeholders.

Intent-Code Divergence

High
Category
Not specified by scanner
Confidence
97% confidence
Finding

The spec says Mai must not claim escrow, direct payment execution, or refund completion without external evidence, but later grants admins payment release/refund authority. This contradiction can lead implementers or operators to perform or represent regulated payment/custody actions without a clearly defined external PSP-controlled workflow, creating both fraud and compliance risk.

Content

No source excerpt is available for this finding.

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · tests/test_install.py (reported line 13)May include surrounding context.

python
class MaiInstallTest(unittest.TestCase):
    def run_install(self, home, *args):
        env = os.environ.copy()
        env["HOME"] = str(home)
        return subprocess.run(
            ["bash", str(ROOT / "scripts" / "install.sh"), *args],

Internal Network Request

Medium
Category
Server-Side Request Forgery
Confidence
70% confidence
Finding

Code issues a request to a loopback, link-local, or private-range host. This can reach internal services not meant to be exposed and is a common SSRF pivot.

Content

Scanner excerpt · Dockerfile (reported line 15)May include surrounding context.

dockerfile
EXPOSE 8765

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health', timeout=2).read()"

CMD ["python3", "scripts/mai_registry.py", "serve", "--data", "/data/mai-registry.json", "--host", "0.0.0.0", "--port", "8765", "--rate-limit-per-minute", "60"]

Session Persistence

Medium
Category
Rogue Agent
Confidence
60% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

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

md
# Mai

Mai is a local-first AI shopping matchmaking agent for OpenClaw and Hermes. Merchants can publish products and manage inventory; buyers can discover merchants and products, compare prices, discuss with sellers, read reviews, and create trackable orders.

Mai records transactions but does not custody funds directly. Local mode tracks external payment URLs and references. Registry mode records PSP-backed payment holds, releases, and refunds so the agent can support escrow-like workflows without pretending to be a licensed payment processor.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding

The skill advertises substantial capabilities including shell execution, network access, and filesystem interaction but does not declare any explicit tool scope or permissions boundary. In an agent ecosystem, this increases the chance that the host grants broader access than users expect, enabling unintended file changes, network calls, or command execution through normal skill use.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
77% confidence
Finding

The skill explicitly supports local-first transaction tracking and registry-backed order/payment state, which implies persistent storage of buyer, merchant, order, message, and payment-reference data. Session persistence is not inherently unsafe, but in a commerce context it can expose sensitive transactional history, contacts, and payment-related metadata if retention, access control, or storage protections are weak.

Content

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

md
---
name: mai
version: 1.1.4
description: "AI shopping matchmaking agent for OpenClaw and Hermes. Use when merchants want to publish products, manage stock, answer buyer questions, and handle order requests; or when buyers want to discover merchants and products, compare prices, discuss with sellers, read reviews, and create trackable orders. Supports local-first transaction tracking and registry-backed PSP custody records."
---

# Mai

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The spec materially expands a local-first shopping assistant into a hosted public marketplace with registry APIs, moderation, and payment-related administrative actions. That architectural expansion increases attack surface substantially and introduces security-sensitive responsibilities without corresponding concrete controls for authz design, auditability, abuse handling, or operational trust boundaries.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
86% confidence
Finding

The spec describes synchronizing buyer/merchant messages and draft orders through a hosted registry but does not require explicit user notice, consent, or data handling limits. Because these records can contain personal, commercial, and potentially sensitive transaction details, silent synchronization creates privacy and legal exposure if users do not understand what leaves the local store.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

This code persists registry state to disk via path.write_text, and the service later calls save_registry from many request handlers, including read-oriented flows such as rate-limit bookkeeping and search endpoints. Although the module docstring describes marketplace behavior, there is no nearby comment, prompt, or user-facing disclosure that operating this service will continuously modify the specified JSON data file.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
77% confidence
Finding

The handler reads bearer tokens or X-Mai-Api-Key values from incoming requests, which is sensitive credential handling. In this file there is no user-facing warning, logging message, or explanatory comment near the server flow informing operators that the service expects secrets in headers and authenticates requests against stored API-key records.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · tests/test_install.py (reported line 15)May include surrounding context.

python
def run_install(self, home, *args):
        env = os.environ.copy()
        env["HOME"] = str(home)
        return subprocess.run(
            ["bash", str(ROOT / "scripts" / "install.sh"), *args],
            cwd=str(ROOT),
            env=env,

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The spec mentions IP-based rate limiting for public search but omits any notice that IP addresses may be collected, stored, or processed for abuse prevention. This is a lower-severity privacy issue, but in a public marketplace context it still matters because network identifiers can be personal data in some jurisdictions and may be retained without transparency.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
81% confidence
Finding

This markdown file documents hosted registry actions and lists MAI_API_KEY as an environment fallback, but it does not warn users that the skill/plugin may use credentials and communicate with a hosted service. For markdown files, missing disclosures about privacy- or system-impacting behavior should be flagged when the description omits them.

Content

No source excerpt is available for this finding.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
plugins/mai-plugin/openclaw_compat.js:74