Back to skill

Security audit

Todolist Md Clawdbot Copy

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its todo-file purpose, but it asks for broad Google Drive authority and has under-scoped write-back behavior that users should review before installing.

Install only if you are comfortable granting broad Google Drive access to this workflow. Prefer running it with a dedicated Google account or reduced OAuth scope, review every suggestions JSON before apply mode, and avoid folders containing Markdown files that should not be AI-reviewed until opt-in enforcement and target-file validation are added.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/todolist_agent_entrypoint.mjs:150
Finding
Managed OAuth Requests Unrestricted Google Drive Access<![CDATA[ ## Vulnerability Details **File Location**: `scripts/todolist_agent_entrypoint.mjs:150-177`; `scripts/todolist_drive_folder_agent.mjs:101-129` **Vulnerability Type**: Excessive OAuth permissions **Risk Level**: High ### Vulnerable Code ```javascript // scripts/todolist_agent_entrypoint.mjs async function ensureManagedOAuth({ refreshTokenFile, clientId, clientSecret, authCode }) { // Returns { refresh_token } when available. if (fs.existsSync(refreshTokenFile)) { const existing = JSON.parse(fs.readFileSync(refreshTokenFile, 'utf8')); if (existing?.refresh_token) return existing; } const scopes = ['https://www.googleapis.com/auth/drive']; if (!authCode) { const url = buildAuthUrl({ clientId, scopes }); return { needsAuth: true, authUrl: url, refreshTokenFile, scope: scopes, howTo: `Open the URL, approve, then rerun with: --authCode <CODE>` }; } const tokens = await exchangeAuthCodeForTokens({ clientId, clientSecret, code: authCode }); if (!tokens.refresh_token) { throw new Error('No refresh_token returned. Try again with prompt=consent and ensure you approved access.'); } const toSave = { created_at: new Date().toISOString(), scopes, refresh_token: tokens.refresh_token, }; ``` ```javascript // scripts/todolist_drive_folder_agent.mjs async function ensureManagedOAuth({ refreshTokenFile, clientId, clientSecret, authCode }) { if (fs.existsSync(refreshTokenFile)) { const existing = JSON.parse(fs.readFileSync(refreshTokenFile, 'utf8')); if (existing?.refresh_token) return existing; } const scopes = ['https://www.googleapis.com/auth/drive']; if (!authCode) { const url = buildAuthUrl({ clientId, scopes }); return { needsAuth: true, authUrl: url, refreshTokenFile, scope: scopes, howTo: `Open the URL, approve, then rerun with: --authCode <CODE>` }; } const tokens = await exchangeAuthCodeForTokens({ clientId ...[truncated 2281 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace the unrestricted `drive` scope with `https://www.googleapis.com/auth/drive.file` wherever the workflow can operate on files explicitly selected or created through the application. 2. Use an explicit file-selection or consent workflow so users grant access only to intended todo files. 3. Separate preparation and write-back permissions. Read-only operations should not automatically receive write access. 4. Bind each write operation to a previously selected and validated file. 5. Clearly display the requested scope and affected resources before authorization. 6. Invalidate existing refresh tokens issued with the unrestricted scope after deploying the reduced-scope implementation. 7. Continue storing refresh tokens with restrictive permissions, and avoid returning token values in logs or error messages. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/todolist_drive_folder_agent.mjs:347
Finding
Per-File Review Opt-In Policy Is Documented but Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:71-95`; `scripts/todolist_drive_folder_agent.mjs:347-348`; `scripts/todolist_drive_folder_agent.mjs:380-387` **Vulnerability Type**: Missing authorization and privacy-policy enforcement **Risk Level**: Medium ### Vulnerable Code and Declared Policy The Skill documentation states that files must be selected through configuration or an in-file marker: ```markdown The agent should: - Download `.todolist-md.config.json` when it changes. - Only review files that match include/exclude rules. ``` The implementation instead selects every Markdown file in the folder: ```javascript let mdFiles = files.filter(f => (f?.mimeType === 'text/markdown') || (f?.name || '').endsWith('.md')); if (onlyName) mdFiles = mdFiles.filter(f => f.name === onlyName); ``` Every changed selected file is subsequently downloaded and included in the request-generation workflow: ```javascript // Build compact LLM request payload for OpenClaw to process. const items = []; for (const f of changed) { const fileId = f.id; const text = await driveDownloadText({ fileId, accessToken }); const openTasks = extractOpenTasks(text, 120); items.push({ fileId, name: f.name, sectionTitle, openTasks, // helpful for applying gate later hint: { modifiedTime: f.modifiedTime, size: f.size, } }); } ``` ### Technical Analysis The documentation presents two mechanisms for controlling review eligibility: - Include and exclude rules in `.todolist-md.config.json`. - An `<!-- bot: ai_enabled --> true` marker in an individual Markdown file. The folder agent does not download or parse the configuration file and does not verify the in-file marker. Its actual selection criterion is only the MIME type or `.md` suffix, with an optional exact filename filter supplied at runtime. Consequently, the documented opt-in boundary is not an effective security control. Every changed Markdown file in the selected fol ...[truncated 1389 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Adopt a default-deny policy: do not download or extract content from a Markdown file unless it is explicitly opted in. 2. Retrieve and validate `.todolist-md.config.json` before selecting review candidates. 3. Apply include and exclude rules before downloading file contents whenever metadata permits. 4. If marker-based opt-in is used, download only enough content to validate the marker before performing task extraction. 5. Define deterministic precedence between folder configuration and in-file markers. 6. Reject malformed configuration rather than silently falling back to processing all Markdown files. 7. Record the opt-in decision and applicable configuration version in the preparation manifest. 8. Add automated tests demonstrating that excluded files and files without the required marker are never included in `llm_request.json`. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/todolist_drive_folder_agent.mjs:421
Finding
Apply Mode Can Modify Arbitrary Drive File IDs from Untrusted Suggestions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/todolist_drive_folder_agent.mjs:421-459` **Vulnerability Type**: Missing object-level authorization for Drive write-back **Risk Level**: High ### Vulnerable Code ```javascript // APPLY mode intentionally does NOT require Drive "changed" detection. // We apply suggestions to the fileIds explicitly provided in suggestionsIn. const results = []; for (const s of sugg.items || []) { const fileId = s.fileId; const suggested = sanitizeBotBlock(s.suggested_markdown || ''); if (!fileId || !suggested) { results.push({ fileId, name: s.name, action: 'skip_missing_suggestion' }); continue; } const meta1 = await driveGetMetadata({ fileId, accessToken }); const text = await driveDownloadText({ fileId, accessToken }); const edited = ensureBotSuggestedSection(text, sectionTitle, suggested); if (edited === text) { results.push({ fileId, name: s.name, action: 'no_change' }); continue; } const meta2 = await driveGetMetadata({ fileId, accessToken }); if (meta2.headRevisionId && meta1.headRevisionId && meta2.headRevisionId !== meta1.headRevisionId) { results.push({ fileId, name: s.name, action: 'skip_due_to_remote_change', before: meta1.headRevisionId, now: meta2.headRevisionId }); continue; } if (dryRun) { results.push({ fileId, name: s.name, action: 'dry_run_would_update', botPreview: suggested.slice(0, 300) }); continue; } const mimeType = meta1.mimeType || 'text/markdown'; const name = meta1.name || s.name || 'todo.md'; await driveUpdateText({ fileId, accessToken, name, mimeType, text: edited }); results.push({ fileId, name: s.name, action: 'updated' }); } ``` ### Technical Analysis Apply mode trusts each `fileId` in `suggestionsIn` and performs authenticated metadata retrieval, content download, transformation, and update against that identifier. It does not verify that the file: - Belongs to the folder supplied through `folderId`. - Appear ...[truncated 2185 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a preparation manifest containing the exact allowed file IDs, folder ID, opt-in decision, expected name, MIME type, revision ID, modification time, and content hash. 2. In apply mode, reject every suggestion whose file ID is absent from that manifest. 3. Retrieve parent metadata and verify that each target remains inside the selected folder. 4. Verify that the target is still opted in and is a supported Markdown file. 5. Compare the current revision or content hash with the value captured during preparation, not merely with a second metadata request made during apply mode. 6. Cryptographically authenticate the manifest and suggestion association, or retain the allowlist in a protected local state file that untrusted model output cannot modify. 7. Validate that each expected file ID occurs at most once and reject unknown or duplicate entries. 8. Use the narrower `drive.file` OAuth scope to reduce the impact of any future object-authorization failure. 9. Consider requiring interactive confirmation that lists every target file before write-back. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (22)

Hidden Instructions

High
Category
Prompt Injection
Content
---
name: todolist-md-clawdbot
description: Operate on todolist-md Markdown todo files. Read + summarize tasks, propose edits, and write outcomes back into Markdown using only <!-- bot: ... --> markers (line-stable write-back).
---

# todolist-md-clawdbot
Confidence
70% confidence
Finding
Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Hidden Instructions

High
Category
Prompt Injection
Content
- Rule: **never insert a new line** once the header exists. Only update the existing header line.
- If the header does not exist, you may insert it at the very top **only if the user explicitly opted into Option B**.
- Canonical format:
  - `<!-- bot: last_review --> 2026-02-04T15:39Z root=<rootFolderId> model=<model>`

6) Never complete tasks without explicit user confirmation.
Confidence
70% confidence
Finding
Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Hidden Instructions

High
Category
Prompt Injection
Content
* Design goals:
 * - Node-only I/O pipeline (unified)
 * - Minimum tokens: do not call LLM unless a file changed; extract only open tasks
 * - Safe write-back: write ONLY into a dedicated bot section using <!-- bot: ... --> markers
 * - Avoid duplicates: overwrite-update via Drive API files.update(fileId)
 * - Avoid overwriting while you edit: revision gate via headRevisionId
 *
Confidence
70% confidence
Finding
Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Credential Access

High
Category
Privilege Escalation
Content
const envFile = '/root/clawd/.secrets/gog.env';

  let account = process.env.GOG_ACCOUNT || '';
  let pw = process.env.GOG_KEYRING_PASSWORD || '';

  if ((!account || !pw) && fs.existsSync(envFile)) {
    const text = fs.readFileSync(envFile, 'utf8');
Confidence
94% confidence
Finding
The code accesses a fixed secret file and reads a keyring password into process memory for later use, which is a genuine credential-access behavior. In a skill context this is more dangerous because the skill can leverage host-resident secrets without explicit user action, potentially enabling unauthorized access to Google Drive accounts or other integrated services.

Credential Access

High
Category
Privilege Escalation
Content
let v = m[2];
      v = v.replace(/^"|"$/g, '');
      if (k === 'GOG_ACCOUNT' && !account) account = v;
      if (k === 'GOG_KEYRING_PASSWORD' && !pw) pw = v;
      if (k === 'GOG_BIN' && !process.env.GOG_BIN) process.env.GOG_BIN = v;
    }
  }
Confidence
93% confidence
Finding
This line specifically extracts and assigns the keyring password from the secrets file, confirming the script is parsing and consuming sensitive credentials. Such behavior is high-risk in an agent environment because it allows the skill to operationalize stored secrets automatically and invisibly.

Credential Access

High
Category
Privilege Escalation
Content
const cmd = [
    'sudo','-u','ubuntu','-H','env',
    `GOG_ACCOUNT=${account}`,
    `GOG_KEYRING_PASSWORD=${pw}`,
    gogBin,
    'drive','ls','--parent', folderId,'--json'
  ];
Confidence
95% confidence
Finding
The script passes account identifiers and a keyring password directly into a subprocess environment, increasing exposure of credentials to child processes, debugging tools, process inspection, and logs. Combined with sudo and external Drive operations, this creates a strong capability for the skill to act using privileged stored credentials.

Hidden Instructions

High
Category
Prompt Injection
Content
`- [ ] Review open tasks in ${fileName} (generated ${now})`,
  ];
  if (openTasks.trim()) {
    lines.push(`  > <!-- bot: note --> Open tasks sampled: ${openTasks.split(/\r?\n/).length}`);
  } else {
    lines.push(`  > <!-- bot: note --> No open tasks found`);
  }
Confidence
70% confidence
Finding
Hidden instructions were detected in comments or invisible text. These could contain malicious directives. Manual review is recommended.

Credential Access

High
Category
Privilege Escalation
Content
def gog_env() -> Dict[str, str]:
    # Use same pattern as TOOLS.md: run as ubuntu and pass GOG_ACCOUNT/GOG_KEYRING_PASSWORD.
    # Here we just forward current environment; caller should export vars.
    env = os.environ.copy()
    if not env.get("GOG_BIN"):
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def gog_env() -> Dict[str, str]:
    # Use same pattern as TOOLS.md: run as ubuntu and pass GOG_ACCOUNT/GOG_KEYRING_PASSWORD.
    # Here we just forward current environment; caller should export vars.
    env = os.environ.copy()
    if not env.get("GOG_BIN"):
        env["GOG_BIN"] = "/home/linuxbrew/.linuxbrew/bin/gog"
    return env
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.

Credential Access

High
Category
Privilege Escalation
Content
"-H",
        "env",
        f"GOG_ACCOUNT={env.get('GOG_ACCOUNT','')}",
        f"GOG_KEYRING_PASSWORD={env.get('GOG_KEYRING_PASSWORD','')}",
        gog,
        "drive",
        "ls",
Confidence
92% confidence
Finding
This command explicitly injects GOG_ACCOUNT and GOG_KEYRING_PASSWORD into the environment of a spawned process. Even though that may be needed for authentication, exposing secrets this way can leak them to process metadata, debugging tools, audit logs, or other local observers depending on platform and configuration.

Credential Access

High
Category
Privilege Escalation
Content
"-H",
        "env",
        f"GOG_ACCOUNT={env.get('GOG_ACCOUNT','')}",
        f"GOG_KEYRING_PASSWORD={env.get('GOG_KEYRING_PASSWORD','')}",
        gog,
        "drive",
        "download",
Confidence
92% confidence
Finding
This repeats the same pattern for the download command, passing account and keyring password material into a child process. In a local automation context this increases credential exposure surface without adding compensating protections.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The script requests the full-scope permission `https://www.googleapis.com/auth/drive`, which grants broad read/write access to all files the user can access, even though the documented function is limited to one todo markdown file. If the token is exposed or the script is modified or misused, the excessive scope enables compromise far beyond the intended single file.

External Transmission

Medium
Category
Data Exfiltration
Content
}

async function oauthAccessTokenFromRefresh({ refreshToken, clientId, clientSecret }) {
  const res = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

async function oauthAccessTokenFromRefresh({ refreshToken, clientId, clientSecret }) {
  const res = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

async function oauthAccessTokenFromRefresh({ refreshToken, clientId, clientSecret }) {
  const res = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
}

async function oauthAccessTokenFromRefresh({ refreshToken, clientId, clientSecret }) {
  const res = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The script silently reads credentials from a hard-coded secrets path under /root, creating implicit privilege and secret dependencies that may surprise operators and expand the attack surface. In an agent/skill setting, hidden credential consumption is risky because users may not realize the code can access powerful pre-provisioned secrets and use them against external services.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The script launches a subprocess through sudo to run as another user without any user-facing warning, which is a meaningful privilege-boundary crossing in an automation context. Even though the command arguments are fixed and use execFileSync rather than a shell, invoking sudo can expose sensitive environment values and enable broader system interaction than expected.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def sh(cmd: List[str], env: Dict[str, str] | None = None) -> str:
    out = subprocess.check_output(cmd, env=env, text=True)
    return out
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The script forwards credential-related environment variables to child processes without minimizing exposure or warning the user. While it does not exfiltrate secrets directly, passing secrets through command invocation increases the chance of accidental disclosure through process inspection, logging, crash reports, or misconfigured wrappers.

Tainted flow: 'new' from pathlib.Path.read_text (line 187, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
else:
                # NOTE: actual upload/in-place update should be implemented by the agent using Drive API files.update.
                # We intentionally do not embed secrets/token logic here.
                tmp.write_text(new, encoding="utf-8")
                print(f"[needs-agent-upload] updated local copy for {f.get('name')} ({fid})")

    state["lastScanAtUtc"] = iso_utc_now()
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Intent-Code Divergence

Low
Confidence
91% confidence
Finding
The header states the script does not call any LLM, which is literally true, but it immediately describes generating a compact LLM request JSON and later the code constructs that payload in prepare mode. This creates intent-level ambiguity in the documentation by presenting the script as non-LLM while implementing direct LLM handoff orchestration.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.env_credential_access, suspicious.exposed_secret_literal (+1 more)

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/todolist_drive_folder_agent.mjs:208

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/todolist_agent_entrypoint.mjs:192

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/todolist_drive_folder_agent.mjs:179

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/todolist_agent_entrypoint.mjs:198

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/todolist_drive_folder_agent.mjs:315

Sensitive-looking file read is paired with a network send.

Warn
Code
suspicious.potential_exfiltration
Location
scripts/todolist_agent_entrypoint.mjs:157

Sensitive-looking file read is paired with a network send.

Warn
Code
suspicious.potential_exfiltration
Location
scripts/todolist_drive_folder_agent.mjs:100