Back to skill

Security audit

To-Do

Security checks for vulnerabilities and agentic risk

Overview

This scheduling skill has a coherent reminder purpose, but its implementation gives unsafe command and scheduler control that users should review before installing.

Install only if you are comfortable with this skill creating OS-level scheduled tasks that can later wake the agent with stored instructions. Avoid putting secrets in reminder text, run it as an unprivileged user, and prefer a revised version that uses spawn or execFile, validates inputs, keeps a private registry of its own jobs, and confirms deletion.

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

T09 · Insecure Skill Coding Practices

Error
Location
to-do.js:20
Finding
Shell Command Injection in Task Scheduling<![CDATA[ ## Vulnerability Details **File Location**: `to-do.js`, lines 20-24 and 127-149 **Vulnerability Type**: OS command injection through dynamically constructed scheduler commands **Risk Level**: High ### Vulnerable Code ```js function execute(command) { return new Promise((resolve) => { exec(command, (error, stdout, stderr) => { resolve({ error, stdout: stdout.trim(), stderr: stderr.trim() }); }); }); } ``` ```js const taskId = `OpenClaw_Task_${Date.now()}`; const flatInstruction = instruction.replace(/\n/g, ' - '); // Quote the binary path in case it contains spaces const winCmd = `schtasks /create /tn "${taskId}" /tr "\\"${OPENCLAW_BIN}\\" agent --message \\\\"${flatInstruction}\\\\" --to \\\\"${userId}\\\\" --channel \\\\"${channel}\\\\" --deliver" /sc ONCE /st ${serverLocal.time} /sd ${serverLocal.date} /f`; const res = await execute(winCmd); ``` ```js const safeInstruction = instruction.replace(/'/g, "'\\''"); const agentCommand = `${OPENCLAW_BIN} agent --message '${safeInstruction}' --to '${userId}' --channel '${channel}' --deliver`; const atCmd = `echo "${agentCommand} >> /tmp/to-do.log 2>&1" | TZ="${tz}" at "${formattedTime}"`; const res = await execute(atCmd); ``` ### Technical Analysis The implementation passes dynamically assembled strings to Node.js `child_process.exec()`, which invokes a command shell. Several values incorporated into these commands are externally controlled or environment-controlled: - Scheduled instruction text - User ID - Channel - Time and timezone - `OPENCLAW_BIN` On Unix, escaping only single quotes in the instruction does not protect the outer double-quoted `echo` argument. Shell expansions such as command substitution using `$()` or backticks can still be interpreted by the shell. The timezone, time, routing fields, and executable path are also interpolated without strict validation. On Windows, backslash-based quote construction does not establish a reliable security bo ...[truncated 1619 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace `exec()` with `execFile()` or `spawn()` and pass every argument through an argument array with `shell: false`. 2. On Unix, start `at` directly and provide the scheduled script through the child process's standard input. Do not use an `echo | at` shell pipeline. 3. Invoke `schtasks.exe` directly on Windows with a separate argument array. 4. Validate `userId` and `channel` against narrow allowlists suitable for their expected formats. 5. Verify timezone values using `Intl.DateTimeFormat` and reject invalid identifiers before command execution. 6. Require strict `YYYY-MM-DD HH:mm` input on every platform rather than forwarding arbitrary text to `at`. 7. Require `OPENCLAW_BIN` to be an absolute path, resolve it canonically, verify it is an expected executable, and reject shell control characters. 8. Treat instruction text as opaque data rather than command text. If necessary, store the payload in a permission-restricted file and pass only the safe file path or identifier to the scheduled process. 9. Run the Skill as an unprivileged, dedicated operating-system account. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
to-do.js:215
Finding
Unscoped Scheduler Job Deletion and Command Injection<![CDATA[ ## Vulnerability Details **File Location**: `to-do.js`, lines 215-228 **Vulnerability Type**: Missing task ownership validation and OS command injection **Risk Level**: High ### Vulnerable Code ```js async function remove(id) { if (!id) { console.log("Usage: node skills/to-do/to-do.js delete <id>"); return; } if (platform === 'win32') { const res = await execute(`schtasks /delete /tn "${id}" /f`); if (res.error) console.error("Error:", res.stderr); else console.log(`🗑️ Task ${id} deleted.`); } else { const res = await execute(`atrm ${id}`); if (res.error) console.error("Error:", res.stderr); else console.log(`🗑️ Task #${id} deleted.`); } } ``` ### Technical Analysis The deletion operation accepts an arbitrary identifier and does not verify that the referenced job was created by this Skill. On Unix, any `at` job accessible to the runtime account can be targeted. The identifier is also directly concatenated into a command executed by a shell, without even a numeric format check. On Windows, any task name accepted by the runtime account can be passed to `schtasks /delete`. The implementation does not require the `OpenClaw_Task_` namespace used during creation. Crafted quotes or command metacharacters may additionally break out of the intended task-name argument. The instruction in `SKILL.md` to run `list` before deletion is only procedural guidance. It does not enforce ownership or authorization in the implementation. ### Attack Path 1. An attacker invokes `delete` or `remove` with an identifier under the runtime account's scheduler access. 2. To delete another job, the attacker supplies the identifier of an unrelated `at` or Windows scheduled task. 3. The script performs no registry, namespace, or ownership check. 4. `atrm` or `schtasks /delete` removes the selected job if the runtime account has permission. 5. Alternatively, the attacker supplies shell metach ...[truncated 750 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Keep a permission-restricted registry of every job created by this Skill, including its platform scheduler ID and an unpredictable internal identifier. 2. Refuse deletion unless the requested job exists in that registry and belongs to the current application or user context. 3. On Unix, require the scheduler ID to match `^[0-9]+$` before use. 4. On Windows, require an exact application-controlled namespace and format, such as `^OpenClaw_Task_[0-9]+_[A-Fa-f0-9]+$`. 5. Do not rely on a name prefix alone as proof of ownership; confirm it against the private registry. 6. Invoke `atrm` or `schtasks.exe` through `execFile()`/`spawn()` with argument arrays and `shell: false`. 7. Remove the registry entry only after successful scheduler deletion. 8. Run the scheduler integration under a dedicated, unprivileged account so it cannot manage unrelated system jobs. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
to-do.js:162
Finding
Overbroad Enumeration and Inspection of Unix Scheduler Jobs<![CDATA[ ## Vulnerability Details **File Location**: `to-do.js`, lines 162-204 **Vulnerability Type**: Excessive access to unrelated scheduler jobs **Risk Level**: Medium ### Vulnerable Code ```js async function list() { if (platform === 'win32') { const res = await execute('schtasks /query /fo LIST /tn "OpenClaw_Task_*"'); if (res.error || res.stdout.includes('ERROR:')) { console.log("No pending tasks."); return; } console.log(res.stdout); } else { const res = await execute('atq | sort -k 6,6 -k 3,3 -k 4,4 -k 5,5'); if (!res.stdout) { console.log("No pending tasks."); return; } console.log("ID\tExecution Time\t\t\tTask Description"); console.log("--\t--------------\t\t\t----------------"); const lines = res.stdout.split('\n'); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length < 2) continue; const id = parts[0]; const dateStr = parts.slice(1, 6).join(' '); const detailRes = await execute(`at -c ${id}`); const match = detailRes.stdout.match(/--message '\\[System: Scheduled Task Executed\\] \\\\n- Created at: .*? \\\\n- Scheduled for: .*? \\\\n- Original instruction: (.*?)'/); const matchFallback = detailRes.stdout.match(/Original instruction: (.*?)'/); let desc = match ? match[1] : (matchFallback ? matchFallback[1] : "(Unknown task)"); console.log(`${id}\t${dateStr}\t${desc}`); } } } ``` ### Technical Analysis The Unix implementation calls `atq` without restricting results to tasks created by the Skill. It then invokes `at -c` for every accessible job, causing the complete job body of unrelated scheduled work to be loaded into the process. Although the implementation normally prints only text matched as an original instruction, reading all job bodies is unnecessary ...[truncated 1350 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Maintain a private registry of scheduler IDs created by the Skill. 2. List only IDs found in that registry instead of enumerating every job returned by `atq`. 3. Before inspecting a job, confirm that the identifier is numeric and belongs to the Skill. 4. Store the task description and schedule in the private registry so `at -c` is not required for routine listing. 5. Protect the registry directory with mode `0700` and its files with mode `0600`. 6. Remove stale registry entries after confirming that their corresponding scheduler jobs no longer exist. 7. Use `execFile()` or `spawn()` with argument arrays and `shell: false` for all scheduler queries. 8. Operate under a dedicated account that has no access to unrelated users' jobs. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
to-do.js:145
Finding
Unsafe Predictable Log File in Shared Temporary Directory<![CDATA[ ## Vulnerability Details **File Location**: `to-do.js`, lines 145-149 **Vulnerability Type**: Unsafe temporary-file handling and potential symlink attack **Risk Level**: Medium ### Vulnerable Code ```js const safeInstruction = instruction.replace(/'/g, "'\\''"); const agentCommand = `${OPENCLAW_BIN} agent --message '${safeInstruction}' --to '${userId}' --channel '${channel}' --deliver`; const atCmd = `echo "${agentCommand} >> /tmp/to-do.log 2>&1" | TZ="${tz}" at "${formattedTime}"`; const res = await execute(atCmd); ``` ### Technical Analysis Every Unix scheduled task appends output to the fixed path `/tmp/to-do.log`. `/tmp` is normally shared and writable by multiple local users. The implementation does not safely create the file, verify its ownership or type, reject symbolic links, or establish restrictive permissions. A local attacker may pre-create `/tmp/to-do.log` as a symbolic link or another special filesystem object. When a scheduled task later opens the path for append, the operating system may follow that link and write output to an attacker-selected target, subject to the scheduler account's filesystem permissions. The shared location can also expose agent output to other local users if the resulting file permissions are too broad. Such output may include workflow details, error messages, paths, identifiers, or other sensitive operational data. ### Attack Path 1. A local attacker predicts the fixed `/tmp/to-do.log` path. 2. Before a scheduled task runs, the attacker creates that path as a symbolic link to another file or prepares it with unsafe ownership or permissions. 3. The scheduled command executes as the Skill's operating-system account. 4. Shell redirection opens `/tmp/to-do.log` in append mode and may follow the attacker-created link. 5. Output is redirected to the selected target or becomes accessible through the shared log file. ### Impact Assessment Depending on the runtime account's privileges and filesystem protection ...[truncated 357 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not place application logs at a fixed path in a shared temporary directory. 2. Use an application-owned state or log directory, such as a directory beneath the runtime user's data directory, with mode `0700`. 3. Create log files with mode `0600` and no-follow semantics. 4. Verify that the file is a regular file owned by the expected account before writing. 5. Prefer the operating system's logging facility or a securely configured application logger. 6. Use a separate log per task if isolation is required, with unpredictable names generated by a secure temporary-file API. 7. Apply log rotation and retention controls, and avoid writing sensitive instruction or routing data unless operationally necessary. 8. Run scheduled tasks as a dedicated unprivileged account to limit the targets available to any filesystem redirection attack. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
Findings (15)

Chaining Abuse

High
Category
Tool Misuse
Content
- ERROR: `at` not found
  - CAUSE: Linux/macOS `atd` daemon not running
  - FIX: `sudo systemctl enable atd && sudo systemctl start atd`

- ERROR: Task fires but agent has NO CONTEXT
  - CAUSE: Vague instruction scheduled
Confidence
75% confidence
Finding
Tool calls are chained to bypass individual safety checks or escalate capabilities beyond what any single tool call would allow.

Credential Access

High
Category
Privilege Escalation
Content
if (!OPENCLAW_TZ) missing.push('OPENCLAW_TZ');
    if (missing.length) {
        console.error(`❌ Missing required environment variable(s): ${missing.join(', ')}`);
        console.error(`   Set them in your .env or shell profile.`);
        console.error(`   Example:`);
        console.error(`     OPENCLAW_BIN=/usr/bin/openclaw`);
        console.error(`     OPENCLAW_TZ=America/Mexico_City`);
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
}

    if (platform === 'win32') {
        const res = await execute(`schtasks /delete /tn "${id}" /f`);
        if (res.error) console.error("Error:", res.stderr);
        else console.log(`🗑️ Task ${id} deleted.`);
    } else {
Confidence
97% confidence
Finding
The Windows deletion path interpolates the user-supplied id directly into a shell command executed via exec. An attacker who controls id can inject additional shell syntax and execute arbitrary commands, while also deleting arbitrary scheduled tasks.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill invokes environment-dependent behavior via required variables like OPENCLAW_BIN and OPENCLAW_TZ, but it does not declare an explicit tool/permission scope. That weakens least-privilege boundaries and makes it harder for a host platform to constrain what resources the skill may access or execute.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill explicitly instructs users to include full names, emails, file paths, URLs, and routing identifiers in scheduled instructions, but it does not warn that these details may be stored and later replayed or disclosed when the task executes. In a scheduling skill, this context persistence materially increases privacy and data-leak risk.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
- ERROR: `at` not found
  - CAUSE: Linux/macOS `atd` daemon not running
  - FIX: `sudo systemctl enable atd && sudo systemctl start atd`

- ERROR: Task fires but agent has NO CONTEXT
  - CAUSE: Vague instruction scheduled
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Session Persistence

Medium
Category
Rogue Agent
Content
- ERROR: `at` not found
  - CAUSE: Linux/macOS `atd` daemon not running
  - FIX: `sudo systemctl enable atd && sudo systemctl start atd`

- ERROR: Task fires but agent has NO CONTEXT
  - CAUSE: Vague instruction scheduled
Confidence
80% 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.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The helper wraps child_process.exec and is later used with command strings that include user-controlled fields such as task text, IDs, channel, timezone, and scheduling arguments. Because exec invokes a shell, this creates a command-injection sink and grants broader host command execution than a reminder skill should need.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The skill can enumerate and remove host-level scheduled jobs, extending beyond simple reminder creation into broader scheduler administration. In an agent context, this increases blast radius: a prompt or caller that reaches these commands may disclose task contents or delete jobs without sufficient authorization boundaries.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
Deletion of scheduled tasks happens immediately with no confirmation, dry run, or ownership validation. In a conversational agent setting, accidental invocation, prompt abuse, or confused-deputy behavior could remove legitimate scheduled work and cause denial of service or missed notifications.

Missing User Warnings

Low
Confidence
82% confidence
Finding
The documented delete command removes scheduled tasks but provides no warning that deletion is destructive and may not be recoverable. This can cause accidental loss of reminders or workflows, especially because the skill encourages task management through IDs.

Context-Inappropriate Capability

Low
Confidence
71% confidence
Finding
A scheduling/reminder skill can reasonably manage time data, but reading process environment state—especially an executable path used to launch another program—is an additional capability not disclosed in the manifest description. The manifest presents user-facing reminder behavior, not dependency on privileged environment configuration to run external binaries.

Natural-Language Policy Violations

Low
Confidence
92% confidence
Finding
The code hard-codes `en-US` in `Intl.DateTimeFormat`, and also uses `toLocaleString('en-US', ...)` for displayed timestamps. This imposes a specific locale on formatted output without offering the user a language or locale choice or documenting a justified locale restriction.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The scheduled-task metadata uses `toLocaleString('en-US', { timeZone: tz })`, which forces U.S. English locale formatting in generated content. Because no alternative locale option is provided, this is a natural-language locale policy concern.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The `now` command formats and prints the current time using `toLocaleString('en-US', ...)`, which fixes the output to a specific locale. The skill does not provide any opt-in or configuration mechanism for other languages/locales.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
to-do.js:29