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.
