T05 · Unauthorized Access and Privilege Escalation
- Location
src/service.py:192- Finding
Cross-Chat Reminder Cancellation Due to Missing Scope Authorization
- Content
View full analysis
Vulnerability Details
File Location:
src/service.py:192-237(with the unscoped cancellation interface exposed atsrc/main.py:99-118)
Vulnerability Type: Broken object-level authorization / missing channel and chat ownership validation
Risk Level: HighVulnerable 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
View remediation
Remediation Suggestions
-
Change every cancellation interface to require the requesting
channelandtovalues: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: ... -
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] -
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 invokingopenclaw cron rm. -
Reject missing, malformed, or mismatched ownership metadata. Do not treat an absent description or scope tag as authorized.
-
Update
src/main.pyso 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> -
Do not return IDs from globally matched jobs. Duplicate-name diagnostics must only include jobs already verified as belonging to the requesting scope.
-
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.
-
