Back to skill

Security audit

PingCode

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent PingCode integration, but it handles credentials and project data in ways users should review before installation.

Install only if you trust the environment and the PingCode application scopes are tightly limited. Treat the client secret as sensitive, avoid running through proxies or logging systems that may record URLs, and be careful with update_workitem.py because it can change remote project data without confirmation and may resolve abbreviated IDs ambiguously.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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 (3)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate_weekly_report.py:26
Finding
Client Secret Transmitted in URL Query Parameters## Vulnerability Details **File Locations**: - `scripts/generate_weekly_report.py:26-34` - `scripts/get_my_tasks.py:25-33` - `scripts/get_projects.py:25-33` - `scripts/get_project_workitems.py:25-33` - `scripts/update_workitem.py:26-34` - `references/api_docs.md:7-9` **Vulnerability Type**: Sensitive credential exposure through URL query parameters **Risk Level**: Medium ### Vulnerable Code The same authentication pattern is used by all five scripts: ```python url = f"{BASE_URL}/v1/auth/token" params = { "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET } try: response = requests.get(url, params=params, timeout=30) ``` The accompanying API documentation explicitly recommends the same pattern: ```http GET https://open.pingcode.com/v1/auth/token?grant_type=client_credentials&client_id={client_id}&client_secret={client_secret} ``` ### Technical Analysis Passing `client_secret` through `requests.get(..., params=params)` serializes the secret into the request URL. TLS protects the request while it is in transit, but it does not prevent the complete URL from being recorded at endpoints or infrastructure that processes the request. Query strings may be captured by HTTP client diagnostics, reverse-proxy access logs, application performance monitoring systems, exception telemetry, network debugging tools, or server-side request logs. Unlike an authorization header, query parameters are commonly treated as ordinary request metadata and may not be redacted automatically. The scripts avoid printing `response.text`, but that precaution does not address exposure of the outbound request URL. ### Attack Path 1. An operator configures a valid `PINGCODE_CLIENT_ID` and `PINGCODE_CLIENT_SECRET`. 2. The operator executes any of the five scripts. 3. The script issues a GET request whose URL contains both credentials. 4. A proxy, telemetry age ...[truncated 1011 chars]
Remediation
## Remediation Suggestions 1. Use PingCode's supported POST-based token exchange, placing credentials in the request body or an authorization header rather than the URL: ```python response = requests.post( f"{BASE_URL}/v1/auth/token", data={ "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, }, timeout=30, ) ``` 2. If PingCode supports HTTP Basic client authentication, prefer `auth=(CLIENT_ID, CLIENT_SECRET)` and omit the credentials from the URL and request body. 3. Confirm the exact supported authentication method against the current official PingCode documentation before deployment. 4. Apply the corrected authentication implementation consistently to all five scripts, preferably through one shared API client module. 5. Update `references/api_docs.md` so it no longer instructs users to place secrets in query strings. 6. Configure HTTP, proxy, and telemetry logging to redact `client_id`, `client_secret`, access tokens, and `Authorization` headers. 7. Rotate any client secret that may already have appeared in logs and review access logs for unauthorized token requests. 8. Grant the application only the minimum PingCode scopes required by these scripts.

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/get_my_tasks.py:42
Finding
“My Tasks” Command Retrieves Work Items Without a Server-Side Assignee Restriction## Vulnerability Details **File Location**: `scripts/get_my_tasks.py:42-67` **Vulnerability Type**: Overbroad data retrieval and missing server-side authorization scope **Risk Level**: Medium ### Vulnerable Code ```python def get_my_tasks(access_token, limit=20): """获取我的工作项列表""" url = f"{BASE_URL}/v1/project/work_items" headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } params = { "page_size": limit, "page_index": 0 } try: response = requests.get(url, headers=headers, params=params, timeout=30) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: print(f"获取任务失败: {e}", file=sys.stderr) return None def format_tasks(data, assignee_filter=None): """格式化任务输出""" if not data or "values" not in data: return "没有找到任务" tasks = data["values"] ``` The optional assignee filter is applied only after the broad result set has already been downloaded: ```python if assignee_filter: filtered_tasks = [] for task in tasks: assignee_obj = task.get("assignee") or {} assignee_name = "" if isinstance(assignee_obj, dict): assignee_name = assignee_obj.get("display_name", "") # 模糊匹配 if assignee_filter.lower() in assignee_name.lower(): filtered_tasks.append(task) tasks = filtered_tasks ``` ### Technical Analysis The command is named and documented as retrieving the current user's tasks, but its API request includes only pagination parameters. It does not provide a current-user identifier, an assignee identifier, or another server-side restriction. Consequently, the records returned are determined by the enterprise application's API permissions rather than the narrower purpose communicated to the user. The opt ...[truncated 1591 chars]
Remediation
## Remediation Suggestions 1. Resolve the authenticated user's immutable PingCode user ID through an appropriate identity endpoint. 2. Include that user ID in the work-item API request as a server-side assignee filter. Do not rely on display-name matching. 3. If the endpoint offers a dedicated “current user's work items” operation, use that endpoint instead of the general collection. 4. Configure the PingCode application with the narrowest possible read scopes and project access. 5. Reject execution if the API cannot guarantee user-scoped results, or rename and document the command as retrieving all work items visible to the application. 6. Keep client-side filtering only as an additional presentation feature, not as an access-control mechanism. 7. Validate `--limit` against the documented API range to prevent unexpectedly broad requests. 8. Add tests that verify the outbound request includes the authenticated user's assignee identifier and that unrelated work items are never returned to the formatter.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/update_workitem.py:43
Finding
Ambiguous Work-Item Prefix Matching Can Modify the Wrong Record## Vulnerability Details **File Locations**: - `scripts/update_workitem.py:43-59` - `scripts/update_workitem.py:103` - `scripts/update_workitem.py:127-131` - `scripts/update_workitem.py:166` **Vulnerability Type**: Unsafe identifier resolution before a destructive API operation **Risk Level**: Medium ### Vulnerable Code The lookup accepts any matching prefix and immediately returns the first result: ```python def get_workitem_detail(access_token, workitem_id): """获取工作项详情""" url = f"{BASE_URL}/v1/project/work_items" headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } params = {"page_size": 100} try: response = requests.get(url, headers=headers, params=params, timeout=30) response.raise_for_status() data = response.json() for item in data.get("values", []): item_id = item.get("id", "") if item_id == workitem_id or item_id.startswith(workitem_id): return item return None ``` The command-line interface explicitly permits abbreviated identifiers: ```python parser.add_argument('--workitem_id', required=True, help='工作项 ID(完整 ID 或前 8 位)') ``` The selected identifier is then used for the update: ```python workitem_id = workitem.get("id") workitem_title = workitem.get("title", "未命名") print(f"找到工作项: [{workitem_id[:8]}] {workitem_title}") ``` ```python result = update_workitem(token, workitem_id, updates) ``` ### Technical Analysis Prefix matching does not prove that an abbreviated identifier uniquely identifies one work item. The implementation returns the first matching record from the first page of up to 100 items and never checks whether another ID shares the same prefix. Although the help text refers to the first eight characters, the code does not enforce an eight-character minimum. A caller can provide a much s ...[truncated 1620 chars]
Remediation
## Remediation Suggestions 1. Require the complete immutable work-item ID for every update operation. 2. Prefer a direct detail endpoint such as `/v1/project/work_items/{full_id}` rather than enumerating a collection. 3. If abbreviated IDs must remain supported: - Enforce a documented minimum prefix length. - Gather every matching item across all relevant pages. - Proceed only when exactly one match exists. - Reject zero matches and multiple matches with a clear error. 4. Require explicit confirmation showing the full ID, title, project, and proposed changes before sending the PATCH request, with a separate noninteractive flag for trusted automation. 5. Add optimistic concurrency control, such as a version or ETag precondition, if supported by PingCode. 6. Record an audit entry containing the requested identifier, resolved full identifier, fields changed, and API result without logging credentials or access tokens. 7. Add tests covering one-character prefixes, colliding eight-character prefixes, absent records, pagination, and direct full-ID lookup.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • 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 (22)

Tainted flow: 'params' from os.environ.get (line 26, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
params = {"page_size": 100, "page_index": 0}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        data = response.json()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 26, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
params = {"page_size": 100, "page_index": 0}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        data = response.json()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 26, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
params = {"page_size": 100, "page_index": 0}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        data = response.json()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 26, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
params = {"page_size": 100, "page_index": 0}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        data = response.json()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 26, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, timeout=30)
        response.raise_for_status()
        result = response.json()
        return result.get("access_token")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 26, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, timeout=30)
        response.raise_for_status()
        result = response.json()
        return result.get("access_token")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 26, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, timeout=30)
        response.raise_for_status()
        result = response.json()
        return result.get("access_token")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 27, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, timeout=30)
        response.raise_for_status()
        result = response.json()
        return result.get("access_token")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 27, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, timeout=30)
        response.raise_for_status()
        result = response.json()
        return result.get("access_token")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 27, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
params = {"page_size": 100}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        data = response.json()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 27, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
params = {"page_size": 100}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        data = response.json()
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'params' from os.environ.get (line 27, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
params = {"page_size": 100}
    
    try:
        response = requests.get(url, headers=headers, params=params, timeout=30)
        response.raise_for_status()
        data = response.json()
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
92% confidence
Finding
声明描述的是较广泛的 PingCode 研发管理集成能力,包括工作项查询、周报生成和项目进度管理;但该代码块的实际功能非常单一,仅用于认证并获取项目列表。它没有实现工作项查询接口、周报生成逻辑,也没有任何项目进度更新/管理能力。虽然“PingCode API 集成”这一高层描述与代码所属领域一致,但就此代码块而言,声明明显夸大了已实现能力,属于描述与行为不一致。

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises capabilities that require environment access, network access, and local file writing, but it does not declare an explicit tool scope such as permissions or allowed-tools. This weakens containment and review because an execution environment may grant broader capabilities than users expect, increasing the chance of unintended data access or outbound requests.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The skill documents commands that can update remote work items and write reports to local files, but it does not clearly warn users that these actions modify project data or create files on disk. In an agent setting, unclear signaling around state-changing or file-writing operations can lead to accidental tampering, overwrites, or unauthorized workflow changes.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
This code file contains user-facing natural-language text that forces a specific language/locale, including the module description and later CLI/output strings. The policy allows locale constraints only when justified or when the user is given a choice, neither of which is present here.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The script retrieves and prints potentially sensitive project-management data, including work item titles, statuses, priorities, and assignee display names, directly to stdout without any confirmation, masking, or sensitivity warning. In an agent/automation context, stdout may be captured by logs, chat transcripts, or downstream tools, creating an unintended data exposure path for internal operational information and personal data.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
This is a natural-language policy issue because the skill forces a specific language for its description, CLI help, and error output. Users who do not read Chinese are not given an opt-in choice or alternative locale, and no region-specific justification is stated in the file.

Missing User Warnings

Low
Confidence
78% confidence
Finding
The markdown explains how to set `PINGCODE_CLIENT_ID` and `PINGCODE_CLIENT_SECRET`, which are sensitive credentials enabling API access to organizational data. Although it mentions not hardcoding them, it does not explicitly warn users that these credentials grant access to PingCode data and should be handled carefully and only used in trusted environments.

Vague Triggers

Low
Confidence
87% confidence
Finding
This manifest file describes general scenarios such as '研发管理自动化、团队协作、数据分析' but does not specify what exact phrases, commands, or contexts should activate the skill. In a manifest, this kind of broad description can make invocation boundaries unclear and increase the chance of unintended activation.

Description-Behavior Mismatch

Low
Confidence
81% confidence
Finding
The file-level description says this script is for automatically generating project weekly reports, but it also implements a separate `get_iterations` API client. That extra behavior is not referenced in the reporting flow and broadens the script beyond the specific behavior documented in this file.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
The script's docstrings, CLI description, help text, and user-facing messages are all presented in Chinese, with no option to select another language. Under the stated policy, forcing a specific language without offering user choice is a natural-language policy concern.

Static analysis

No suspicious patterns detected.