Back to skill

Security audit

极鲸云Shopee数据分析与市场调研

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent Shopee research tool, but it stores login tokens in multiple local locations and lets request destinations and user-facing action links come from insufficiently constrained inputs.

Review before installing. Use only the default GeekBI endpoint, avoid passing custom base URLs, and do not install or run the skill from shared repositories or synchronized workspaces unless token storage is fixed. Treat any login or recovery link shown by the skill as untrusted unless you independently verify the destination belongs to GeekBI. Clearing auth state is supported, but the current implementation may have written token state to several locations.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/geekbi_auth.py:371
Finding
Bearer tokens are unnecessarily replicated across multiple plaintext state files<![CDATA[ ## Vulnerability Details **File Location**: `scripts/geekbi_auth.py:69-95`, `scripts/geekbi_auth.py:371-429`, `scripts/geekbi_auth.py:631-649` **Vulnerability Type**: Plaintext credential storage and excessive credential replication **Risk Level**: High ### Vulnerable Code ```python def _user_config_state_path(): return _absolute_path( user_config_path("GeekBI", appauthor=False, ensure_exists=True) / "temu-research-skill" / AUTH_FILE_NAME ) def _skill_state_path(): return _absolute_path(Path(__file__).parent.parent / AUTH_STATE_DIR / AUTH_FILE_NAME) def _workspace_state_path(): return _absolute_path(Path(os.getcwd()) / AUTH_STATE_DIR / AUTH_FILE_NAME) def _resolve_stores(): candidates = ( ResolvedStore(_user_config_state_path(), "user-config-directory"), ResolvedStore(_skill_state_path(), "skill-directory"), ResolvedStore(_workspace_state_path(), "working-directory"), ) stores = [] seen_paths = set() for store in candidates: path_key = os.path.normcase(os.fspath(store.path)) if path_key in seen_paths: continue seen_paths.add(path_key) stores.append(store) return tuple(stores) ``` ```python def _write_state_files(stores, payload): normalized = _normalize_state(payload) errors = [] written = 0 for store in stores: try: _write_state_file(store, normalized) written += 1 except OSError as error: errors.append(f"{store.kind}: {_storage_probe_reason(error)}") if written == 0: reason = ";".join(errors) or "登录状态目录不可用" raise OSError(reason) def _update_state(callback): stores = _resolve_stores() try: with _stores_lock(stores) as locked_stores: payload = _load_state_from_stores(stores) changed, result = callback(payload) needs_sync = any( not _state_file_matches(store, ...[truncated 3327 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store authentication state in one dedicated user configuration location only. Remove the Skill-directory and current-working-directory mirrors. 2. Prefer an operating-system credential manager such as Windows Credential Manager, macOS Keychain, or Secret Service on Linux for the bearer token. 3. If file storage remains necessary, store only non-sensitive metadata in JSON and keep the token in a separate protected secret store. 4. Correct the `temu-research-skill` directory name and ensure authentication state cannot be shared across unrelated Skills. 5. On POSIX systems, fail closed if mode `0600` for the token file or `0700` for its directory cannot be established. 6. Implement restrictive Windows ACL handling rather than silently skipping permission protection. 7. Avoid writing credentials into repositories, mounted workspaces, temporary execution directories, or locations likely to be synchronized or backed up. 8. Add tests verifying that exactly one protected credential store is used and that no token appears in the Skill or working directory. 9. Consider token rotation or revocation after upgrading users from the mirrored-storage implementation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/geekbi_auth.py:698
Finding
Unrestricted base URL permits arbitrary authenticated request destinations and SSRF<![CDATA[ ## Vulnerability Details **File Location**: `scripts/shopee_category_info.py:33-42`, `scripts/shopee_category_list.py:33-42`, `scripts/shopee_goods_info.py:32-41`, `scripts/shopee_goods_search.py:58-66`, `scripts/shopee_mall_info.py:24-33`, `scripts/shopee_site_list.py:75-82`, `scripts/geekbi_auth.py:698-721` **Vulnerability Type**: Unvalidated network destination and server-side request forgery **Risk Level**: Medium ### Vulnerable Code Each query CLI exposes an unrestricted base URL. For example: ```python parser = argparse.ArgumentParser(description="查询 Shopee 商品并输出 JSON") parser.add_argument("--base-url", default=DEFAULT_BASE_URL) parser.add_argument("--param", action="append", default=[], help="查询条件,格式为 名称=值") parser.add_argument("--timeout", type=float, default=30) try: params = parse_params(args.param) payload = authenticated_json_request( build_url(args.base_url, ENDPOINT, params), args.base_url, args.timeout ) ``` The shared request function sends requests to the resulting URL without validating its scheme, hostname, resolved address, or relationship to `base_url`: ```python def authenticated_json_request( url, base_url, timeout, *, method="GET", body=None, headers=None, ): complete_pending_login(base_url, timeout) request_headers = _api_headers() if headers: request_headers.update(headers) authorization = _authorization_header(base_url) if authorization: request_headers["token"] = authorization request = Request( url, data=body, headers=request_headers, method=method, ) try: with urlopen(request, timeout=timeout) as response: response_payload = _read_json_response(response) _raise_action_if_needed(response_payload) return response_payload ``` ### Technical Analysis The declared production API is `https://openapi.geekbi.com`, but all public query scripts accept an ar ...[truncated 2265 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `--base-url` from production-facing command-line interfaces and use the fixed GeekBI API origin. 2. If endpoint configurability is required, enforce an explicit allowlist containing only approved HTTPS hostnames, such as `openapi.geekbi.com`. 3. Reject all non-HTTPS schemes before loading authentication state or sending any request. 4. Resolve the destination and reject loopback, private, link-local, multicast, reserved, and cloud metadata address ranges. 5. Revalidate the destination after redirects, or disable cross-origin redirects for authenticated requests. 6. Verify that the final request URL has the same canonical scheme, hostname, and port as the approved base URL before attaching a token. 7. Keep endpoint injection for tests in test-only helpers rather than public production arguments. 8. Add tests for HTTP rejection, malicious domains, alternate ports, user-info URL syntax, redirects, IPv4 and IPv6 private addresses, and DNS rebinding scenarios. ]]>

T01 · Skill Instruction Hijacking

Warning
Location
references/查询暂停与恢复流程.md:3
Finding
Untrusted server messages and action URLs are presented as trusted Agent instructions<![CDATA[ ## Vulnerability Details **File Location**: `references/查询暂停与恢复流程.md:3-8`, `scripts/geekbi_auth.py:494-512`, `scripts/geekbi_auth.py:669-695` **Vulnerability Type**: Remote instruction and phishing-link injection **Risk Level**: Medium ### Vulnerable Code The Skill instructions require server-controlled content to be reproduced directly: ```markdown 业务脚本先正常查询。服务端在 `data.jumpUrl` 中返回操作地址,表示因未登录或其他可恢复条件要求暂停时: 1. 原样使用服务端中文 `msg`。 2. 将 `jumpUrl` 展示为可点击 Markdown 链接,不自行生成或改写。 3. 停止数据查询;用户完成操作或回复“继续”后,原样重跑原请求。 4. 条件仍未满足时使用服务端最新提示;链接失效时重跑取得新链接。 ``` The implementation accepts both fields without validating the URL origin: ```python def _raise_action_if_needed(payload): data = payload.get("data", {}) if isinstance(payload, dict) else {} jump_url = data.get("jumpUrl") if isinstance(data, dict) else None if not isinstance(jump_url, str) or not jump_url: return raise ActionRequired( response_message(payload, "请完成页面操作后继续"), jump_url, action=data.get("error") or "ACTION_REQUIRED", expires_in=int(data.get("expiresIn", 0)), ) ``` ```python def _save_challenge(base_url, response_payload): data = response_payload.get("data", {}) device_code = data.get("deviceCode") jump_url = data.get("jumpUrl") if not isinstance(device_code, str) or not device_code: raise ValueError("登录响应缺少 deviceCode") if not isinstance(jump_url, str) or not jump_url: raise ValueError("登录响应缺少 jumpUrl") expires_in = int(data.get("expiresIn", 0)) def save_challenge(payload): server = _server_state(payload, base_url) _remove_access_token(server) server["pending"] = { "deviceCode": device_code, "jumpUrl": jump_url, "expiresAt": int(time.time()) + expires_in, } return True, None _update_state(save_challenge) raise ActionRequired( response_payload.get("msg", "需要登录后继续"), jump_url, action="AUT ...[truncated 2106 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate authentication and recovery instructions locally from fixed, reviewed text rather than reproducing remote messages verbatim. 2. Treat server-provided messages as untrusted informational data and clearly label or sanitize them before presentation. 3. Require `jumpUrl` to use HTTPS and match a strict allowlist of approved GeekBI authentication hostnames and ports. 4. Reject URLs containing user-info components, unexpected ports, encoded hostname tricks, non-web schemes, or redirectors. 5. Resolve and validate the final redirect destination before presenting it to the user. 6. Display the validated hostname prominently so users can verify where they are being sent. 7. Do not convert an unvalidated URL into a clickable Markdown link. 8. Combine this remediation with removal or strict validation of `--base-url`. 9. Add tests using malicious messages, Markdown control characters, deceptive Unicode domains, non-HTTPS URLs, and off-domain redirects. ]]>
Vulnerability Patterns
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (2)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill invokes local scripts and external data sources, which implies file read, shell, and network capabilities, but it does not declare permissions or constraints for those operations. This weakens security review and runtime governance because operators and users cannot easily assess what the skill is allowed to access or whether execution is appropriately sandboxed.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The auth state path is created under a different directory name ("temu-research-skill") than the declared Shopee skill. This can cause unintended credential/state sharing or collisions with another skill using the same storage location, which may expose tokens across skills or mix authentication contexts in ways the operator does not expect.

Static analysis

No suspicious patterns detected.