Back to skill

Security audit

ticktick-official-cli

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Dida365 task CLI, but it handles OAuth credentials and destructive account changes with weak safeguards that users should review before installing.

Review this before installing if your Dida365 account contains sensitive tasks or projects. Use only the official API base URL, avoid exposing command output in shared logs, restrict permissions on ~/.config/ticktick-official, revoke tokens if they appear in transcripts, and require explicit user approval before delete commands.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (5)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/ticktick_cli.py:232
Finding
OAuth Bearer Token Can Be Redirected to an Arbitrary Server<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ticktick_cli.py:232-236`; related token forwarding occurs in `scripts/ticktick_api_client.py:198-229` **Vulnerability Type**: Unrestricted authenticated API endpoint **Risk Level**: High ### Vulnerable Code ```python base_url: str = typer.Option( DEFAULT_BASE_URL, "--base-url", envvar=ENV_BASE_URL, help="API base URL.", ) ``` ```python def _headers(self) -> dict[str, str]: """Build request headers.""" return { "Authorization": f"Bearer {self.config.token}", "Accept": "application/json", "User-Agent": self.config.user_agent, } def _url(self, path: str) -> str: """Build the complete request URL.""" base_url = str(self.config.base_url) return f"{base_url.rstrip('/')}/{path.lstrip('/')}" def _request( self, method: str, path: str, params: dict[str, str] | None = None, payload: dict[str, Any] | list[Any] | None = None, ) -> httpx.Response: """Send an HTTP request.""" return self.session.request( method=method.upper(), url=self._url(path), params=params, json=payload, headers=self._headers(), timeout=self.config.timeout_seconds, ) ``` ### Technical Analysis The CLI allows the API base URL to be controlled through either `--base-url` or the `TICKTICK_BASE_URL` environment variable. The resulting URL is used for authenticated requests without restricting the destination to the official Dida365 API. Although the base URL is validated as an HTTP URL by Pydantic, that validation does not enforce the official hostname and permits unencrypted HTTP. The client consequently sends the OAuth bearer token to any destination supplied through the option or environment. This exceeds the minimum privileges required by the declared functionality. The Skill states that it connects directly to official Dida365 services, so forwarding authentication to arbitrary origins is un ...[truncated 921 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the configurable base URL if custom endpoints are not required. - Otherwise, enforce HTTPS and an exact hostname allowlist such as `api.dida365.com`. - Reject URLs containing unexpected ports, credentials, fragments, or nonstandard schemes. - Configure redirect handling so authorization headers are never forwarded across origins. - If development endpoints are needed, require an explicit unsafe-development mode and a separate nonproduction token. - Do not allow an ambient environment variable to silently override the authenticated destination. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/ticktick_oauth.py:54
Finding
OAuth Client Secret and Access Token Are Stored Without Enforced Restrictive Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ticktick_oauth.py:54-75` **Vulnerability Type**: Insecure plaintext credential storage **Risk Level**: High ### Vulnerable Code ```python def save_app_config(client_id: str, client_secret: str, redirect_uri: str, path: Path = APP_ENV_FILE) -> None: path.parent.mkdir(parents=True, exist_ok=True) content = ( "# Generated by ticktick_oauth.py\n" f'export {ENV_CLIENT_ID}="{client_id}"\n' f'export {ENV_CLIENT_SECRET}="{client_secret}"\n' f'export {ENV_REDIRECT_URI}="{redirect_uri}"\n' ) path.write_text(content, encoding="utf-8") def save_token(access_token: str, path: Path = TOKEN_ENV_FILE) -> None: path.parent.mkdir(parents=True, exist_ok=True) content = ( "# Generated by ticktick_oauth.py\n" "# Source: official Dida365 OAuth\n" f'export TICKTICK_TOKEN="{access_token}"\n' ) path.write_text(content, encoding="utf-8") ``` ### Technical Analysis The application client secret and OAuth access token are stored as plaintext shell assignments. The files are written with `Path.write_text()` without explicitly creating them with mode `0600` or correcting permissions on pre-existing files. Their effective permissions therefore depend on the process umask and any permissions already assigned to the files. A permissive environment may produce credentials readable by other local accounts or processes. Plaintext storage may be necessary for a basic CLI, but unrestricted file permissions are not. The implementation does not apply the minimum local access controls expected for reusable OAuth credentials. ### Attack Path 1. A user runs `setup`, `exchange`, or `login`. 2. The script creates or replaces `app.env` or `token.env`. 3. A permissive umask or insecure pre-existing file mode leaves the credential file readable by unauthorized local principals. 4. Another local user or compromised process reads the client secret or be ...[truncated 526 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the configuration directory with mode `0700`. - Create credential files atomically with mode `0600`, rather than relying on the current umask. - Verify and repair permissions on existing credential files before reading or overwriting them. - Reject symbolic links and avoid unsafe replacement of attacker-controlled paths. - Prefer an operating-system credential store or keyring for access tokens and client secrets. - Document credential locations, permissions, revocation procedures, and expected token lifetime. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/ticktick_oauth.py:173
Finding
OAuth Access Tokens Are Printed to Standard Output<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ticktick_oauth.py:173-182` and `scripts/ticktick_oauth.py:300-309` **Vulnerability Type**: Sensitive credential exposure through output **Risk Level**: High ### Vulnerable Code ```python access_token = payload["access_token"] console.print("[green]Token exchange succeeded.[/green]") console.print_json(data=payload) if save: save_token(access_token) console.print(f"[green]Token saved:[/green] {TOKEN_ENV_FILE}") console.print("\n[bold]You can run:[/bold]") console.print(f'export TICKTICK_TOKEN="{access_token}"') ``` The same output behavior occurs after the automated `login` flow: ```python access_token = payload["access_token"] console.print("[green]Login succeeded and an access token was obtained.[/green]") console.print_json(data=payload) if save: save_token(access_token) console.print(f"[green]Token saved:[/green] {TOKEN_ENV_FILE}") console.print("\n[bold]You can run:[/bold]") console.print(f'export TICKTICK_TOKEN="{access_token}"') ``` ### Technical Analysis Both OAuth flows print the complete token response and then print the raw access token again in an export command. OAuth responses may contain not only an access token but also metadata or additional sensitive fields returned by the provider. This script is explicitly intended for Agent use. In such environments, standard output can be retained in tool results, model context, conversation transcripts, telemetry, CI logs, shell history captures, or orchestration logs. Printing a reusable bearer token therefore turns a successful login into a credential-disclosure event. Token output is unnecessary because the implementation already saves the token to a local configuration file by default. ### Attack Path 1. An Agent or user invokes `exchange` or `login`. 2. Dida365 returns a valid OAuth access token. 3. The script prints the complete OAuth response and raw token to standard output. 4. The Agent platform, terminal logge ...[truncated 549 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not print the OAuth response or access token by default. - Report only whether authentication succeeded and where the credential was securely stored. - Redact token values in all errors, diagnostics, and structured logs. - If token display is essential, require an explicit warning-gated option such as `--reveal-token`. - Send sensitive output only to a controlled terminal and refuse reveal operations in detected Agent or noninteractive environments. - Add automated tests that verify access tokens and client secrets never appear in stdout or stderr. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/ticktick_oauth.py:2
Finding
Runtime Dependencies Are Not Reproducibly Pinned<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ticktick_oauth.py:2-8` and `scripts/ticktick_cli.py:2-9` **Vulnerability Type**: Unpinned runtime dependency resolution **Risk Level**: Medium ### Vulnerable Code ```python #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.10" # dependencies = [ # "httpx>=0.28.1", # "typer>=0.20.1", # "rich>=14.2.0", # ] # /// ``` The CLI similarly declares: ```python #!/usr/bin/env -S uv run --script # /// script # requires-python = ">=3.14" # dependencies = [ # "httpx>=0.28.1", # "typer>=0.20.1", # "pydantic>=2.12.5", # "rich>=14.2.0", # ] # /// ``` ### Technical Analysis The executable scripts use `uv run --script` with lower-bound-only dependency constraints. Future versions satisfying these ranges may therefore be downloaded and executed even though those versions were not part of the audited artifact. No malicious dependency or unsafe package source was identified in the reviewed code. The risk arises from non-reproducible future resolution: a compromised upstream release, account takeover, or incompatible update could alter behavior at execution time. Because imported Python packages execute initialization code in the Skill's process, dependency compromise can grant code execution with the same local privileges as the Skill. ### Attack Path 1. A future package release satisfying a `>=` constraint is published or compromised. 2. The Skill runs in an environment where the dependency is not already cached and trusted. 3. `uv` resolves and downloads the newer package. 4. Python imports the package as part of normal Skill startup. 5. Malicious package initialization code executes with the user's or Agent's privileges. ### Impact Assessment A compromised dependency could access local files available to the Skill, including its plaintext OAuth configuration and token files. It could also make network requests, alter API operations, or execute arbitrary cod ...[truncated 140 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Pin dependencies to exact reviewed versions. - Use a lockfile or equivalent reproducible resolution mechanism. - Verify package hashes and retrieve packages only from an approved index. - Periodically update dependencies through a reviewed change process rather than accepting new versions automatically. - Run dependency vulnerability and provenance checks in CI. - Consider packaging dependencies in a reviewed immutable environment instead of installing them during Skill execution. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ticktick_cli.py:357
Finding
Destructive Project and Task Deletions Lack an Enforced Confirmation Gate<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ticktick_cli.py:357-363` and `scripts/ticktick_cli.py:555-563` **Vulnerability Type**: Unprotected destructive operation **Risk Level**: Medium ### Vulnerable Code ```python @project_app.command("delete", help="Delete a project.") def project_delete( ctx: typer.Context, project_id: str = typer.Option(..., "--project-id"), ) -> None: client = get_client(ctx) client.delete_project(project_id) console.print("OK") ``` ```python @task_app.command("delete", help="Delete a task.") def task_delete( ctx: typer.Context, project_id: str = typer.Option(..., "--project-id"), task_id: str = typer.Option(..., "--task-id"), ) -> None: client = get_client(ctx) client.delete_task(project_id, task_id) console.print("OK") ``` ### Technical Analysis `SKILL.md` states that project and task deletion is dangerous and should be confirmed before execution. The executable interface does not enforce that requirement. Supplying identifiers immediately results in an authenticated DELETE request. A documentation-only instruction is weaker than a code-level safety boundary, particularly because the CLI is intended primarily for AI Agent invocation. Erroneous argument generation, prompt manipulation, or ambiguous user requests can directly cause destructive operations. ### Attack Path 1. An Agent receives an ambiguous, erroneous, or manipulated request involving a project or task. 2. The Agent invokes `project delete` or `task delete` with the selected identifiers. 3. The CLI performs no interactive confirmation and requires no explicit force flag. 4. The authenticated API request deletes the target data. 5. The CLI prints `OK`, after the destructive action has already occurred. ### Impact Assessment An attacker or mistaken Agent invocation can delete individual tasks or entire projects available to the authenticated account. Project deletion may affect multiple tasks and associa ...[truncated 175 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require interactive confirmation that clearly identifies the object to be deleted. - For noninteractive Agent use, require an explicit `--yes` or `--confirm-delete` flag. - Consider requiring the exact project or task identifier to be repeated as confirmation. - Provide a dry-run mode that shows the target and intended API request without executing it. - Where supported by the service, prefer soft deletion or moving items to a recoverable trash state. - Ensure Agent-facing documentation requires separate user approval immediately before destructive execution. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (24)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared description emphasizes official Dida365 task/project management, but the actual behavior includes local credential setup, browser-based OAuth, localhost callback handling, and token persistence that are not transparently disclosed as primary capabilities. This mismatch is dangerous because reviewers and users may approve the skill for routine task management without realizing it also handles secrets, opens a local listener, and stores long-lived tokens on disk.

Credential Access

High
Category
Privilege Escalation
Content
## [Authorization](#/openapi?id=authorization)

### [Get Access Token](#/openapi?id=get-access-token)

In order to call Dida365's Open API, it is necessary to obtain an access token for the corresponding user. Dida365 uses the OAuth2 protocol to obtain the access token.
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### [Get Access Token](#/openapi?id=get-access-token)

In order to call Dida365's Open API, it is necessary to obtain an access token for the corresponding user. Dida365 uses the OAuth2 protocol to obtain the access token.

#### [First Step](#/openapi?id=first-step)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### [Get Access Token](#/openapi?id=get-access-token)

In order to call Dida365's Open API, it is necessary to obtain an access token for the corresponding user. Dida365 uses the OAuth2 protocol to obtain the access token.

#### [First Step](#/openapi?id=first-step)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### [Get Access Token](#/openapi?id=get-access-token)

In order to call Dida365's Open API, it is necessary to obtain an access token for the corresponding user. Dida365 uses the OAuth2 protocol to obtain the access token.

#### [First Step](#/openapi?id=first-step)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
| Name | Description |
| --- | --- |
| code | Authorization code for subsequent access tokens |
| state | state parameter passed in the first step |

#### [Third Step](#/openapi?id=third-step)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
#### [Third Step](#/openapi?id=third-step)

To exchange the authorization code for an access token, make a POST request to https://dida365.com/oauth/token with the following parameters(Content-Type: application/x-www-form-urlencoded):

| Name | Description |
| --- | --- |
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
#### [Third Step](#/openapi?id=third-step)

To exchange the authorization code for an access token, make a POST request to https://dida365.com/oauth/token with the following parameters(Content-Type: application/x-www-form-urlencoded):

| Name | Description |
| --- | --- |
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
#### [Third Step](#/openapi?id=third-step)

To exchange the authorization code for an access token, make a POST request to https://dida365.com/oauth/token with the following parameters(Content-Type: application/x-www-form-urlencoded):

| Name | Description |
| --- | --- |
Confidence
70% 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
#### [Delete Task](#/openapi?id=delete-task)

```
DELETE /open/v1/project/{projectId}/task/{taskId}
```

##### [Parameters](#/openapi?id=parameters-4)
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
###### [Request](#/openapi?id=request-4)

```
DELETE /open/v1/project/{{projectId}}/task/{{taskId}} HTTP/1.1
Host: api.dida365.com
Authorization: Bearer {{token}}
```
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
#### [Delete Project](#/openapi?id=delete-project)

```
DELETE /open/v1/project/{projectId}
```

##### [Parameters](#/openapi?id=parameters-9)
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
###### [Request](#/openapi?id=request-9)

```
DELETE /open/v1/project/{{projectId}} HTTP/1.1
Host: api.dida365.com
Authorization: Bearer {{token}}
```
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill invokes scripts that perform local file reads/writes and networked OAuth/API operations, but it declares no explicit tool scope or permissions. That omission weakens reviewability and consent boundaries, making it easier for an agent or user to trigger sensitive actions like token persistence and outbound connections without clear upfront disclosure.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The skill description is written to be used in Chinese and explicitly targets Dida365, with no indication that users may choose another language or locale. The policy requires flagging language or locale constraints when they are imposed without user opt-in or clear justification.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
This markdown file describes a DELETE endpoint that removes a task, but it does not include any warning that the action affects user data or may be irreversible. Under the markdown-specific warning criteria, destructive behaviors should be clearly disclosed to users.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The Delete Project endpoint describes deletion of a user project but provides no accompanying warning about potential loss of project data and related contents. For markdown skill documentation, behaviors that can materially affect user data should be explicitly called out.

External Transmission

Medium
Category
Data Exfiltration
Content
import httpx
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field

DEFAULT_BASE_URL = "https://api.dida365.com/open/v1"


class ApiModel(BaseModel):
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
81% confidence
Finding
This code file performs outbound HTTP requests and attaches the configured OAuth access token in the Authorization header, which is a sensitive credential and may transmit user or system data to an external service. While the code has developer-facing docstrings, it lacks any user-facing confirmation, warning, or visible logging that would disclose this behavior to an end user of the skill.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
This code presents command help and runtime messages in Chinese for users, e.g. command descriptions and error guidance, while the skill does not offer an opt-in language selection. That creates a natural-language locale policy issue because it effectively forces one language for interaction.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
When token setup fails, the user receives Chinese-only recovery instructions. Because this is a core operational path and no locale choice is provided, the file imposes a specific language on users without opt-in.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
Multiple command descriptions and status messages throughout the CLI are Chinese-only, including project/task actions and doctor output guidance. Across the file, this constitutes a forced locale for user-facing interaction rather than an isolated string.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script persists the OAuth client secret and access token to predictable files under ~/.config/ticktick-official without setting restrictive permissions or clearly warning the user that long-lived credentials are being stored on disk. On multi-user systems, misconfigured home-directory permissions, backups, sync tools, or other local compromise could expose these secrets and allow unauthorized access to the user's Dida365 account or reuse of the client credentials.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The CLI help string and many user-facing messages are written only in Chinese, which imposes a specific language on all users. The file does not provide an opt-in language selection or indicate that the tool is intentionally limited to a Chinese-speaking audience.

Static analysis

No suspicious patterns detected.