Back to skill

Security audit

Todoist API

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent Todoist automation skill, but its token handling is too broad because an invoked command could send a user's Todoist token to a caller-chosen base URL.

Review before installing. Use this only with a Todoist token you are willing to grant to the agent, prefer environment-based secret injection over --token, do not use --base-url except for the official Todoist HTTPS API, and require dry-runs plus explicit confirmation before bulk, raw, sync, or destructive changes. The strongest mitigation would be to remove or allowlist --base-url and avoid command-line token passing.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/todoist_api.py:207
Finding
Bearer Token Can Be Transmitted to an Arbitrary or Plaintext Destination## Vulnerability Details **File Location**: `scripts/todoist_api.py:139-143, 207-217, 2300-2301`; `scripts/smoke_test.py:37-52` **Vulnerability Type**: Unrestricted credential destination and insecure transport configuration **Risk Level**: High ### Vulnerable Code `scripts/todoist_api.py:139-143`: ```python def with_base(base_url: str, path: str) -> str: base = base_url.rstrip("/") if not path.startswith("/"): path = "/" + path return f"{base}{path}" ``` `scripts/todoist_api.py:207-217`: ```python url = with_base(base_url, path) if query: encoded_query = urllib.parse.urlencode( {k: v for k, v in query.items() if v is not None}, doseq=True, ) if encoded_query: url = f"{url}?{encoded_query}" headers = { "Authorization": f"Bearer {token}", ``` `scripts/todoist_api.py:2300-2301`: ```python parser.add_argument("--token", help="Todoist API token. Defaults to TODOIST_API_TOKEN.") parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help=f"API base URL (default: {DEFAULT_BASE_URL})") ``` `scripts/smoke_test.py:37-52`: ```python parser.add_argument("--token", help="Todoist API token. Defaults to TODOIST_API_TOKEN.") parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help=f"API base URL (default: {DEFAULT_BASE_URL})") parser.add_argument("--timeout", type=int, default=20, help="HTTP timeout in seconds (default: 20).") args = parser.parse_args(argv) token = args.token or os.getenv("TODOIST_API_TOKEN") or os.getenv("TODOIST_TOKEN") if not token: emit({"ok": False, "error": "Missing Todoist token. Pass --token or set TODOIST_API_TOKEN."}) return 2 url = args.base_url.rstrip("/") + "/projects?" + urllib.parse.urlencode({"limit": 1}) request = urllib.request.Request( url=url, method="GET", ...[truncated 2611 chars]
Remediation
## Remediation Suggestions 1. Remove `--base-url` from production-facing commands unless custom endpoints are an essential requirement. 2. Centralize endpoint validation and apply it to both scripts before retrieving or attaching the token. 3. Require the `https` scheme and reject embedded credentials, fragments, unexpected ports, and malformed URLs. 4. Allowlist the exact official API origin, such as `https://api.todoist.com`, including the expected API path prefix. 5. If development endpoints are required, place them behind an explicit unsafe-development option and require a separate non-production credential. 6. Disable redirects for authenticated requests or validate every redirect target and strip authorization whenever the scheme, hostname, or port changes. 7. Add tests proving that HTTP URLs, lookalike domains, subdomain tricks, user-info URLs, and cross-origin redirects are rejected. 8. Document that agent-generated or user-supplied URLs must never determine the destination of a request carrying a Todoist credential.

T09 · Insecure Skill Coding Practices

Note
Location
scripts/smoke_test.py:10
Finding
Todoist Tokens May Be Exposed Through Process Command-Line Arguments## Vulnerability Details **File Location**: `scripts/todoist_api.py:2300`; `scripts/smoke_test.py:10-13, 37` **Vulnerability Type**: Sensitive credential exposure through process arguments **Risk Level**: Low ### Vulnerable Code `scripts/todoist_api.py:2300`: ```python parser.add_argument("--token", help="Todoist API token. Defaults to TODOIST_API_TOKEN.") ``` `scripts/smoke_test.py:10-13`: ```python Usage: python3 scripts/smoke_test.py python3 scripts/smoke_test.py --token "$TODOIST_API_TOKEN" ``` `scripts/smoke_test.py:37`: ```python parser.add_argument("--token", help="Todoist API token. Defaults to TODOIST_API_TOKEN.") ``` ### Technical Analysis Both scripts permit the Todoist bearer token to be supplied through the `--token` command-line option. The smoke-test documentation explicitly demonstrates expanding the secret into that argument. Command-line arguments can be exposed through operating-system process inspection, shell tracing, audit telemetry, job-runner metadata, crash reports, terminal logs, and automation-platform execution records. Although access to process details depends on the host's security configuration, command-line arguments are not an appropriate secret transport mechanism. The scripts already support environment-based token discovery, so command-line token input is not necessary for their core functionality. ### Attack Path 1. A user follows the documented example or an automation system invokes a script with `--token`. 2. The shell expands the secret into the process argument vector. 3. During execution, a local user, monitoring agent, process collector, or job platform captures the command line. 4. The exposed token remains available in process telemetry or retained logs. 5. An attacker with access to that data extracts the token. 6. The attacker reuses it against the Todoist API. ### Impact Assessment The resulting access is bounded by the compr ...[truncated 373 chars]
Remediation
## Remediation Suggestions 1. Remove the `--token` option from both scripts. 2. Prefer protected environment injection, a permission-restricted credential file, operating-system secret storage, or standard input where appropriate. 3. Remove documentation examples that expand secrets into command arguments. 4. If backward compatibility requires retaining `--token`, mark it as deprecated and emit a clear warning that it may expose the token through process listings and logs. 5. Ensure CI systems and agent runners mask Todoist credentials and do not print environment values or expanded commands. 6. Advise users to rotate the token if it has previously appeared in process telemetry, shell tracing, or execution logs.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (30)

Tainted flow: 'request' from os.getenv (line 48, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
)

    try:
        with urllib.request.urlopen(request, timeout=args.timeout) as response:
            body = response.read().decode("utf-8", errors="replace")
            payload = json.loads(body) if body else {}
    except urllib.error.HTTPError as exc:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'request' from os.getenv (line 233, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
stderr(f"{method} {url} (attempt {attempt + 1}/{retry + 1})")
        request = urllib.request.Request(url=url, data=data, headers=headers, method=method)
        try:
            with urllib.request.urlopen(request, timeout=timeout) as response:
                raw = response.read()
                if not raw:
                    return None
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
This is a material description-to-behavior mismatch. The description promises a broad Todoist management skill with many user-facing capabilities across tasks, projects, comments, reports, templates, migration, and automation. The actual code chunk does none of that: it is a narrow diagnostic utility that reads a token, calls a single read-only projects endpoint, checks pagination fields, and returns structured status output. While this code is related to Todoist and could support the larger skill as a test/helper, the chunk itself has a substantially different primary purpose and lacks the declared management behaviors.

Credential Access

High
Category
Privilege Escalation
Content
names to IDs, bulk-close or move tasks, add repeated comments, review completed work,
  manage project structure, export templates, or automate Todoist workflows.
license: MIT. See LICENSE.txt
compatibility: Requires HTTPS access to api.todoist.com plus Python 3.9+ or curl. Write operations require a Todoist API token or OAuth access token.
metadata:
  author: OpenAI
  version: "2.0.0"
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- **Command catalogue and endpoint coverage** → [references/REFERENCE.md](references/REFERENCE.md)
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill exposes capabilities that can read environment variables, access the filesystem, write files, and make network requests, but it does not declare any explicit tool scope or permission boundaries. In an agent setting, this increases the risk of overbroad execution, accidental token exposure, unintended file writes, or unauthorized outbound API use because consumers cannot easily constrain what the skill is allowed to touch.

External Transmission

Medium
Category
Data Exfiltration
Content
import urllib.parse
import urllib.request

DEFAULT_BASE_URL = "https://api.todoist.com/api/v1"
USER_AGENT = "todoist-api-skill-smoke/2.0.0"
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
import urllib.parse
import urllib.request

DEFAULT_BASE_URL = "https://api.todoist.com/api/v1"
USER_AGENT = "todoist-api-skill-smoke/2.0.0"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.