Back to skill

Security audit

Remind Me

Security checks for vulnerabilities and agentic risk

Overview

This reminder skill mostly matches its stated purpose, but its cancellation code can delete reminders without verifying they belong to the current chat.

Review this skill before installing in any shared or multi-chat OpenClaw environment. Its reminder creation and listing behavior is coherent, but cancellation should be fixed to require channel/chat ownership checks before deleting by name or ID. Users should also understand that timezone preferences may be read from or saved to USER.md.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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 (1)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
src/service.py:192
Finding
Cross-Chat Reminder Cancellation Due to Missing Scope Authorization## Vulnerability Details **File Location**: `src/service.py:192-237` (with the unscoped cancellation interface exposed at `src/main.py:99-118`) **Vulnerability Type**: Broken object-level authorization / missing channel and chat ownership validation **Risk Level**: High ### Vulnerable Code ```python def cancel_reminder_by_name(name: str) -> str: """Cancel a reminder by its human-readable name. Returns a plain-text confirmation or error string. """ jobs, error = _fetch_all_jobs() if error: return f"Error: Could not fetch reminders.\n{error}" matched = [j for j in jobs if j.get("name") == name] if not matched: return f"Error: No reminder found with name '{name}'." if len(matched) > 1: ids = ", ".join(j["id"] for j in matched) return ( f"Error: Multiple reminders found with name '{name}'. " f"Use the ID to cancel a specific one.\nIDs: {ids}" ) return cancel_reminder_by_id(matched[0]["id"], display_name=name) def cancel_reminder_by_id(job_id: str, display_name: str = "") -> str: """Cancel a reminder by its job ID. Returns a plain-text confirmation or error string. """ code, out, err = _run([OPENCLAW_BIN, "cron", "rm", job_id]) if code != 0: return f"Error: Failed to cancel reminder '{job_id}'.\n{err or out}" label = f"'{display_name}'" if display_name else f"ID {job_id}" return f"✅ Reminder {label} has been cancelled." ``` The command-line dispatcher also omits the requesting channel and chat identity: ```python elif command == "cancel": # cancel id <jobId> | cancel name <name> if len(args) < 3: print( "Usage: remind-me cancel id <jobId> OR remind-me cancel name <name>" ) sys.exit(1) by = args[1].lower() value = args[2] if by == "id": p ...[truncated 3043 chars]
Remediation
## Remediation Suggestions 1. Change every cancellation interface to require the requesting `channel` and `to` values: ```python def cancel_reminder_by_name(name: str, channel: str, to: str) -> str: ... def cancel_reminder_by_id( job_id: str, channel: str, to: str, display_name: str = "" ) -> str: ... ``` 2. For name-based cancellation, filter jobs by ownership before matching: ```python jobs, error = _fetch_all_jobs() scoped_jobs = _jobs_for_channel(jobs, channel, to) matched = [job for job in scoped_jobs if job.get("name") == name] ``` 3. For ID-based cancellation, fetch the target from the job list and verify that its description contains the exact scope tag generated by `build_channel_tag(channel, to)` before invoking `openclaw cron rm`. 4. Reject missing, malformed, or mismatched ownership metadata. Do not treat an absent description or scope tag as authorized. 5. Update `src/main.py` so cancellation commands include channel and chat identity, consistent with create and list operations: ```text remind-me cancel id <jobId> <channel> <to> remind-me cancel name <name> <channel> <to> ``` 6. Do not return IDs from globally matched jobs. Duplicate-name diagnostics must only include jobs already verified as belonging to the requesting scope. 7. Add tests proving that: - A chat can cancel its own reminder by name and ID. - The same operations fail for reminders owned by another chat. - Duplicate names in different chats do not disclose foreign IDs. - Jobs without valid ownership tags cannot be removed through this Skill.
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (11)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
This finding indicates the implementation performs global job enumeration (`cron list --all`) and allows cancellation by name without origin scoping, despite claiming per-chat isolation. That creates a direct risk of exposing metadata about other users' jobs internally and deleting reminders outside the current chat context if names collide or matching is overly broad.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
This finding indicates the implementation performs global job enumeration (`cron list --all`) and allows cancellation by name without origin scoping, despite claiming per-chat isolation. That creates a direct risk of exposing metadata about other users' jobs internally and deleting reminders outside the current chat context if names collide or matching is overly broad.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
cancel_reminder_by_name fetches all jobs globally and matches only on the human-readable name, without verifying that the job belongs to the requesting channel/chat. Because the skill promises channel-scoped reminders, a user who knows or guesses another reminder name could cancel reminders outside their own chat, violating authorization boundaries.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
cancel_reminder_by_id directly deletes any job ID via the backend without checking that the ID belongs to the originating channel/chat. If a user can obtain a job ID from another context, they can perform unauthorized deletion of reminders across chats, which is especially dangerous in a multi-tenant scheduling skill.

Lp3

Medium
Category
MCP Least Privilege
Confidence
86% confidence
Finding
The skill invokes shell commands (`uv run ... main.py ...`) but does not declare any explicit tool scope such as `permissions` or `allowed-tools`. That weakens least-privilege controls and can cause the runtime to grant broader shell capability than users or reviewers expect, increasing the blast radius if the skill is abused or prompt-injected.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
- **WHAT is missing** → Ask: "What would you like me to remind you about?"
- **WHEN is missing AND cannot be reasonably assumed** → Ask: "How often, or at what time?"
- **WHEN is missing BUT can be reasonably assumed as once** → Assume one-shot, but confirm: "Just once, right?"
- **WHERE is always auto-detected** → Never ask the user for this. Read it from session context (see below).
- **TIMEZONE is always auto-detected** → Never ask unless unresolvable (see Step 2 below).

### Do NOT create the job until all four are confirmed.
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
- **WHAT is missing** → Ask: "What would you like me to remind you about?"
- **WHEN is missing AND cannot be reasonably assumed** → Ask: "How often, or at what time?"
- **WHEN is missing BUT can be reasonably assumed as once** → Assume one-shot, but confirm: "Just once, right?"
- **WHERE is always auto-detected** → Never ask the user for this. Read it from session context (see below).
- **TIMEZONE is always auto-detected** → Never ask unless unresolvable (see Step 2 below).

### Do NOT create the job until all four are confirmed.
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill directs silent reading of `USER.md` to obtain timezone information without disclosing access to persistent profile data. Even if the data is low sensitivity, hidden reads from long-lived user profiles reduce transparency and may surprise users who believe the agent is using only the current conversation.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The skill instructs the agent to persist the user's timezone into `USER.md` after asking whether to remember it, but it does not clearly disclose that this writes profile data to disk. Storing personal preference data without transparent notice can violate privacy expectations and create unintended retention of user metadata.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def _run(cmd: list[str]) -> tuple[int, str, str]:
    """Run a subprocess command and return (returncode, stdout, stderr)."""
    result = subprocess.run(cmd, capture_output=True, text=True)
    return result.returncode, result.stdout.strip(), result.stderr.strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The function removes a scheduled job via `openclaw cron rm` and immediately returns success or failure, but there is no confirmation prompt or explicit warning comment/docstring indicating that this action deletes an existing reminder. Because this is a destructive operation, the code should provide clearer disclosure before or at the point of execution unless such warning is documented elsewhere.

Static analysis

No suspicious patterns detected.