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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/smoke_test.py (reported line 59)May include surrounding context.

python
)

    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:

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/todoist_api.py (reported line 235)May include surrounding context.

python
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

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.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 10)May include surrounding context.

md
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"

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 73)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 79)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 80)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 81)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 102)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 103)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 109)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 110)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 111)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 119)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 126)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 134)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 142)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 150)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 160)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 186)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 194)May include surrounding context.

md
- **`scripts/todoist_api.py`** — main non-interactive Todoist CLI

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 177)May include surrounding context.

md
- **Command catalogue and endpoint coverage** → [references/REFERENCE.md](references/REFERENCE.md)

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/smoke_test.py (reported line 26)May include surrounding context.

python
import urllib.parse
import urllib.request

DEFAULT_BASE_URL = "https://api.todoist.com/api/v1"
USER_AGENT = "todoist-api-skill-smoke/2.0.0"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/todoist_api.py (reported line 36)May include surrounding context.

python
import urllib.parse
import urllib.request

DEFAULT_BASE_URL = "https://api.todoist.com/api/v1"
USER_AGENT = "todoist-api-skill-smoke/2.0.0"