Back to skill

Security audit

Todolist Md Clawdbot

Security checks for vulnerabilities and agentic risk

Overview

This Drive todo helper is mostly purpose-aligned, but it uses broad Google Drive authority and weak file/credential scoping that users should review before installing.

Review before installing. Use only with a dedicated Google account or isolated Drive folder containing no unrelated files, keep suggestions JSON trusted, prefer dry-run first, and revoke/delete stored OAuth tokens if you stop using it. Do not point it at a broad personal or business Drive until OAuth scope, file opt-in enforcement, and credential handling are tightened.

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_drive_folder_agent.mjs:99
Finding
Managed OAuth requests unrestricted access to the user's entire Google Drive<![CDATA[ ## Vulnerability Details **File Location**: `scripts/todolist_drive_folder_agent.mjs:99-104`; `scripts/todolist_agent_entrypoint.mjs:156-161` **Vulnerability Type**: Excessive OAuth permissions **Risk Level**: High ### Vulnerable Code ```javascript // scripts/todolist_drive_folder_agent.mjs:99-104 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']; ``` ```javascript // scripts/todolist_agent_entrypoint.mjs:156-161 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']; ``` ### Technical Analysis Both managed OAuth implementations request the unrestricted Google Drive scope. This scope permits the application to view, download, create, modify, and potentially delete files throughout the authorized user's Drive, subject to Google Drive API behavior and the user's existing permissions. The declared functionality is limited to reviewing and updating selected Markdown todo files. Full-Drive authorization therefore exceeds the minimum privilege normally required. The scripts retain a refresh token, so the excessive authorization is not limited to a single execution. The network exchanges themselves are directed to the official Google OAuth endpoint and are necessary for OAuth authentication. The security issue is the breadth of the requested scope, not the destination of the requests. ### Attack Path 1. A user runs either script and follows the managed OAuth authorization ...[truncated 1079 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace the unrestricted Drive scope with `https://www.googleapis.com/auth/drive.file` where the workflow can operate on files explicitly created or selected through the application. 2. Require explicit user selection or authorization for every file the Skill may process. 3. If folder-wide access cannot be implemented with a narrower scope, use a dedicated Google account or isolated Drive folder containing no unrelated content. 4. Clearly disclose any unavoidable broad permission before authorization rather than describing the OAuth mode only as managed or recommended. 5. Enforce folder membership, Markdown type, and per-file opt-in checks independently of OAuth scope before every download or update. 6. Store refresh tokens in an operating-system credential store or secrets manager when available, retain restrictive file permissions as defense in depth, and provide a documented revocation procedure. 7. Detect previously stored tokens authorized with the broad scope and require reauthorization with the reduced scope. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/todolist_drive_folder_agent.mjs:424
Finding
Apply mode accepts arbitrary Drive file IDs without validating folder membership or authorization<![CDATA[ ## Vulnerability Details **File Location**: `scripts/todolist_drive_folder_agent.mjs:424-463` **Vulnerability Type**: Unvalidated authorization target and confused-deputy file modification **Risk Level**: High ### Vulnerable Code ```javascript const sugg = loadJsonOrDefault(suggestionsIn, null); if (!sugg || sugg.schema !== 'todolist-md.llm_suggestions.v1') { throw new Error('Invalid suggestions JSON. Expect schema todolist-md.llm_suggestions.v1'); } // 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' }); } ...[truncated 2435 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a prepare manifest containing the authorized file IDs, root folder ID, expected MIME types, revisions, and content hashes. 2. Require apply mode to consume that exact manifest and reject every suggestion whose file ID is absent. 3. Bind suggestions to the manifest using an unpredictable run identifier or an authenticated signature when the handoff crosses a trust boundary. 4. Before reading or writing each target, retrieve its metadata with parent information and verify that it remains inside the configured root folder. 5. Require a Markdown MIME type or an explicitly permitted `.md` filename rather than inheriting an arbitrary target MIME type. 6. Verify the expected `headRevisionId` from prepare time, not merely two metadata calls made during apply. 7. Enforce per-file opt-in during both prepare and apply. 8. Default to rejection when authorization evidence, parent metadata, revision data, or opt-in state is missing. 9. Use the narrower `drive.file` OAuth scope to reduce the impact of any remaining validation error. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/todolist_drive_folder_agent.mjs:337
Finding
Folder runner ignores the documented per-file AI opt-in controls<![CDATA[ ## Vulnerability Details **File Location**: `scripts/todolist_drive_folder_agent.mjs:337-380`; documented policy at `SKILL.md:62-90` **Vulnerability Type**: Missing consent and file-selection enforcement **Risk Level**: Medium ### Vulnerable Code ```javascript let mdFiles = files.filter(f => (f?.mimeType === 'text/markdown') || (f?.name || '').endsWith('.md') ); if (onlyName) mdFiles = mdFiles.filter(f => f.name === onlyName); const changed = []; for (const f of mdFiles) { const fid = f.id; if (!fid) continue; const prev = stFiles[fid] || {}; if (prev.modifiedTime !== f.modifiedTime || prev.size !== f.size) { changed.push(f); } } ``` ```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, } }); } ``` The documented selection policy states: ```markdown The agent should: - Download `.todolist-md.config.json` when it changes. - Only review files that match include/exclude rules. ``` It also documents marker-based selection: ```markdown The agent should only review files containing that marker. ``` ### Technical Analysis The folder runner selects all files that either advertise a Markdown MIME type or have a `.md` suffix. It does not load `.todolist-md.config.json`, evaluate its include/exclude rules, or require the documented `<!-- bot: ai_enabled --> true` marker. The optional `onlyName` argument can narrow execution manually, but it does not implement the stated consent mechanism and is not enabled by default. Consequently, every changed Markdown file in the listed folder is treated as eligible for download and task extraction. The genera ...[truncated 1311 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Load `.todolist-md.config.json` before selecting files and enforce its `ai.enabled`, `include`, and `exclude` values. 2. If marker mode is selected, require an exact `<!-- bot: ai_enabled --> true` marker before extracting or forwarding task content. 3. Default to deny when neither a valid configuration nor an explicit marker authorizes review. 4. Apply identical authorization checks in prepare and apply modes so an excluded file cannot later be targeted through suggestions. 5. Treat malformed configuration as an error or skip all files rather than silently processing every Markdown file. 6. Record the authorization mechanism and configuration hash in the prepare manifest for later verification. 7. Clearly report skipped and authorized files without including their task content. 8. Add tests covering excluded filenames, disabled AI configuration, absent markers, malformed configuration, and apply attempts against non-opted-in files. ]]>
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 (26)

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.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The script reads extra secret material from /root/clawd/.secrets/gog.env and passes account and keyring password values into a subprocess invocation for an unrelated gog tool. In an agent-skill context, this expands the trust boundary far beyond a simple Drive todo updater, enabling access to local secrets and privileged command execution paths that could be abused if the script or environment is compromised.

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
98% confidence
Finding
The code accesses a local secrets file under /root/clawd/.secrets to retrieve gog account credentials and keyring password, which is unrelated to the core todo-processing task. In an agent environment, harvesting or depending on extra credentials materially increases the blast radius because compromise of this skill can expose credentials for additional systems.

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
97% confidence
Finding
The script explicitly parses and loads GOG_KEYRING_PASSWORD from a local secret source, expanding access to sensitive credentials not required for its primary function. This creates unnecessary credential exposure and makes the skill more dangerous in a privileged automation context.

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
98% confidence
Finding
Passing GOG_ACCOUNT and GOG_KEYRING_PASSWORD into a child process environment exposes sensitive credentials to subprocess execution and potentially to process inspection, logs, crash reports, or inherited environments. In a multi-tenant or agent-hosted system, this is a serious secret-handling weakness with unnecessary lateral-risk potential.

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
89% confidence
Finding
The script injects GOG_ACCOUNT and GOG_KEYRING_PASSWORD directly into the command line via env VAR=value arguments to sudo/env. On many systems command-line arguments are observable to other local users through process listing or audit logs, which can disclose the keyring password and compromise the authenticated Drive account.

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
89% confidence
Finding
This repeats the same credential exposure pattern for the download path: the keyring password is embedded in process arguments when invoking sudo/env/gog. In the context of a Drive-review automation skill, that is more dangerous because the script operates on potentially sensitive documents and the exposed credential may grant broader Drive access than a single file.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill explicitly instructs the agent to persist storage configuration such as local paths, Drive folder IDs, S3 bucket/prefix information, and stable identity keys for future runs. While this is operationally useful, these values can reveal sensitive filesystem layout, cloud resource identifiers, and repository locations if stored insecurely or without user awareness, creating avoidable privacy and targeting risk.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The documentation states 'write ONLY into a dedicated bot section', implying modifications are confined to an existing marked section. However, ensureBotSuggestedSection creates a new titled section and bot marker block when the section is absent, which modifies document structure beyond merely updating an existing dedicated section.

Intent-Code Divergence

Medium
Confidence
93% confidence
Finding
The header says the script 'does NOT call any LLM', which is literally true, but the surrounding documentation presents the script as limited to a non-LLM role while the implementation materially participates in an LLM workflow by creating request JSON in prepare mode and writing LLM suggestions into Drive in apply mode. That documentation understates the script's effective behavior and intent.

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.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The top-level documentation says the script 'only' performs Drive listing, state comparison, and optional header updating. However, the implementation explicitly downloads changed files to /tmp before modifying them, which is a material side effect not reflected in that enumerated behavior.

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
89% confidence
Finding
The code passes GOG_ACCOUNT and GOG_KEYRING_PASSWORD into `sudo ... env` when listing and downloading Drive files, which involves handling sensitive credentials and making networked access to Google Drive. Although the module docstring mentions that `gog` must already be authenticated, there is no user-facing warning, confirmation, or logging near these credential-using operations explaining that credential environment variables are consumed for remote access.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The comment 'Optionally stamp header on changed files' implies the files themselves are updated. In practice, the script downloads the file, edits a local /tmp copy, and prints that agent upload is still needed; no remote Drive update occurs.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script downloads remote Drive content to a predictable path under /tmp using the Drive file ID as the filename, without warning users or using secure temporary-file creation. On multi-user systems this can expose sensitive document contents, enable symlink clobbering attacks, or leave recoverable remnants of downloaded files in a shared temporary directory.

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.

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