Back to skill

Security audit

全行业标书智能检查助手

Security checks for vulnerabilities and agentic risk

Overview

The skill’s bid-document workflow is coherent, but it has under-disclosed local persistence/write scope and credential/privacy handling weaknesses that warrant Review before installation.

Use this skill only if you are comfortable uploading tender and bid documents to biaoshu.zhiliaobiaoxun.com under a paid API account. Create and enter the API key yourself, and do not paste API keys, phone numbers, or SMS codes into chat. Before use, review output paths carefully and be aware that project metadata may be cached locally in ~/.zcm/projects.json until removed.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • 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
  • 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 (6)

T01 · Skill Instruction Hijacking

Error
Location
SKILL.md:140
Finding
Mandatory Promotional Output and Agent Instruction Hijacking<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:140-150` **Additional Locations**: `SKILL.md:63-74`, `SKILL.md:170`; `references/usage.md:7-35` **Vulnerability Type**: Agent instruction hijacking through mandatory output directives **Risk Level**: High ### Vulnerable Code Snippet ```markdown ## 🚫 对用户输出的第一铁律(优先级最高,覆盖本文档其余所有内容) - 本 skill 的一切命令(`python3 …`、`zcm.py …`、`login`、`interpret` 等)**只在后台执行**,**任何情况下不得出现在给用户的回复里** ... - **安装成功后的介绍、或用户问「这个 skill 能干什么 / 怎么拿 Api Key」时,必须完整传达两块信息** ``` The operating manual reinforces these requirements: ```markdown ## ⚠️ 输出约定(必须遵守,除非用户明确说不要) 运行 `zcm.py` 时**老老实实把脚本输出原样给用户看** ``` ### Technical Analysis The Skill declares its own instructions to have overriding priority and prescribes mandatory content for the Agent's final responses. The required content includes branded registration instructions, platform URLs, billing information, feature promotion, examples, status messages, and remaining account balance. Operational instructions may legitimately guide an Agent, but this implementation goes beyond task-specific behavior by declaring that its output rules override the rest of the Skill and must be included for broad classes of user questions. This can alter the Agent's response goals when the Skill is loaded and displace concise, user-directed answers with prescribed promotional content. The instructions also require raw platform URLs to be repeatedly exposed and require large blocks of onboarding and feature material even when a shorter answer would satisfy the user. ### Attack Path 1. The Agent loads `SKILL.md` to process a bid-related request. 2. The embedded rules declare themselves the highest-priority rules within the Skill. 3. A user asks a general question such as how to use the Skill or what it can do. 4. The Agent is instructed to include the complete API-key acquisition process, all major features, examples, branded URLs, and prescribed output language. 5. The resulting response is contr ...[truncated 645 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove all claims that Skill-authored rules have overriding or highest priority. 2. Replace mandatory promotional output with conditional guidance that is included only when directly relevant. 3. Do not require the Agent to reproduce all features and examples in response to a simple usage question. 4. Present registration, billing, and platform links only when the user requests them or when they are necessary to continue an operation. 5. Allow the Agent to summarize script output rather than requiring verbatim relay. 6. Separate security requirements, such as never exposing the API key, from promotional and presentation requirements. 7. Add an explicit rule that user intent and higher-level platform safety policies take precedence over Skill presentation preferences. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/zcm_lib/http_client.py:40
Finding
API Key May Be Forwarded Outside the Approved Domain Through HTTP Redirects<![CDATA[ ## Vulnerability Details **File Location**: `scripts/zcm_lib/http_client.py:40-48` **Additional Location**: `scripts/zcm_lib/cli.py:199-214` **Vulnerability Type**: Credential disclosure through unrestricted automatic redirects **Risk Level**: High ### Vulnerable Code Snippet ```python def request_json(method, url, *, headers=None, data=None, json_body=None, handle_http_error_fn, die, network_error_message): """Send an HTTP request and parse a JSON response.""" hdrs = dict(headers or {}) if json_body is not None: data = json.dumps(json_body).encode("utf-8") hdrs["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=hdrs, method=method) try: with urllib.request.urlopen(req) as resp: ``` The request headers are populated with the account credential here: ```python def _headers(extra=None): h = {"X-App-Key": get_creds()} if extra: h.update(extra) return h ``` ### Technical Analysis `urllib.request.urlopen()` uses Python's default redirect handling. The client neither disables redirects nor validates that a redirect target remains on the approved HTTPS origin. The `X-App-Key` credential is attached as a regular request header. Under default redirect processing, custom headers can be copied to the redirected request. Consequently, a redirect to another origin can expose the API key outside `biaoshu.zhiliaobiaoxun.com`. The fixed initial base URL does not eliminate this risk because the redirect destination is controlled by the HTTP response rather than by `base_url()`. ### Attack Path 1. The client constructs a request to the approved API endpoint. 2. `_headers()` adds the user's `X-App-Key`. 3. The server, reverse proxy, or compromised upstream returns a redirect to an attacker-controlled HTTPS URL. 4. `urllib.request.urlopen()` follows the redirect automatically. 5. The redirected request retains the custom authentication header. 6. The external ...[truncated 664 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable automatic redirects for authenticated API requests. 2. If redirects are required, implement a custom redirect handler that: - Allows only HTTPS. - Requires the exact hostname `biaoshu.zhiliaobiaoxun.com`. - Restricts destinations to the expected API path. - Rejects hostname changes, user-information components, and nonstandard ports. 3. Strip `X-App-Key`, `Authorization`, cookies, and other sensitive headers before every redirected request. 4. Limit the number of permitted redirects. 5. Validate the final response URL before reading or processing its body. 6. Add automated tests for same-origin redirects, cross-origin redirects, downgrade redirects, and redirect loops. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/zcm_lib/storage.py:62
Finding
Undeclared Persistent Project Metadata Written Outside the Allowed Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/zcm_lib/storage.py:62-93` **Additional Locations**: `SKILL.md:16-20`, `SKILL.md:49-52`; `scripts/zcm_lib/cli.py:369-373` **Vulnerability Type**: Write outside the declared least-privilege filesystem boundary **Risk Level**: Medium ### Vulnerable Code Snippet ```python def projects_path(): home = os.environ.get("ZCM_HOME", "").strip() or os.path.join(os.path.expanduser("~"), ".zcm") return os.path.join(home, "projects.json") def _load_projects(): try: with open(projects_path(), "r", encoding="utf-8") as f: data = json.load(f) return data if isinstance(data, dict) else {} except (FileNotFoundError, ValueError, OSError): return {} def remember_tender(tender_filename, project_id=None, job_id=None): """Remember tender filename by project_id/job_id for later report naming.""" if not tender_filename: return name = os.path.basename(str(tender_filename).rstrip("/")) data = _load_projects() by_pid = data.setdefault("by_project", {}) by_job = data.setdefault("by_job", {}) if project_id is not None: by_pid[str(project_id)] = name if job_id is not None: by_job[str(job_id)] = name path = projects_path() try: os.makedirs(os.path.dirname(path), exist_ok=True) fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) ``` The write is triggered after interpretation: ```python tender = args.name or os.path.basename(args.source.rstrip("/")) remember_tender(tender, project_id=pid, job_id=job_id) ``` ### Technical Analysis The Skill declares that writes are limited to the generated-output directory and skill-local `config.json`. In practice, successful interpretation calls `remember_tender()`, which creates or updates `~/.zcm/projects.json` unless `ZCM_HO ...[truncated 1235 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store project metadata only inside the declared output directory. 2. Update the permission declaration if persistent metadata is genuinely required. 3. Inform users before creating persistent metadata and disclose its exact path, contents, and retention period. 4. Add a command to delete all locally cached project metadata. 5. Consider making the cache opt-in and disabled by default. 6. Store only the minimum identifier required; avoid retaining filenames where possible. 7. Use atomic writes and secure directory permissions. 8. Add tests verifying that no write occurs outside the documented filesystem scope. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/report_lib/generator.py:13
Finding
Arbitrary Output Paths Permit File Overwrite Outside the Declared Output Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/report_lib/generator.py:13-38` **Additional Locations**: `scripts/zcm_lib/jobs.py:45-56`; `scripts/zcm_lib/parser.py:128-139`; `scripts/zcm_lib/cli.py:443-450` **Vulnerability Type**: Path traversal and unrestricted file overwrite **Risk Level**: Medium ### Vulnerable Code Snippet ```python def generate(data, service=None, fmt="html", out_dir=".", basename=None, tender_name=None): """Render report files and return output paths.""" detected, result = _unwrap(data) service = service or detected if service not in RENDERERS: raise ValueError(f"未知 service:{service}(应为 interpretation / compliance / bid_duplicate)") html, blocks = RENDERERS[service](result) os.makedirs(out_dir, exist_ok=True) label = _LABEL[service] tender_name = tender_name or _auto_tender_name(service, result) if basename: base = basename elif tender_name: base = f"{_safe_name(tender_name)}_{label}" else: base = f"{label}_{datetime.now():%Y%m%d_%H%M%S}" outs = [] if fmt in ("html", "both"): p = os.path.join(out_dir, base + ".html") with open(p, "w", encoding="utf-8") as f: f.write(html) outs.append(p) if fmt in ("docx", "both"): p = os.path.join(out_dir, base + ".docx") with open(p, "wb") as f: f.write(build_docx(blocks)) ``` Generated documents are also downloaded directly to caller-selected paths: ```python def download_result(job_id, out_path, *, base_url_fn, headers_fn, handle_http_error_fn, die_fn): """Stream a generated .docx result to disk.""" url = base_url_fn() + f"/jobs/{job_id}/result" req = urllib.request.Request(url, headers=headers_fn(), method="GET") try: with urllib.request.urlopen(req) as resp, open(out_path, "wb") as handle: ``` ### Technical Analysis The report generator accepts an unrestricted `out_dir` and an unsaniti ...[truncated 1543 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define one approved output root and resolve it with `realpath()`. 2. Resolve each proposed destination and verify it remains beneath the approved root using `os.path.commonpath()`. 3. Reject absolute basenames, path separators, `..`, null bytes, and platform-specific alternate separators. 4. Apply filename sanitization to explicit basenames as well as automatically derived names. 5. Do not accept arbitrary output directories unless the user explicitly authorizes them and the permission declaration allows them. 6. Use exclusive creation mode or require explicit overwrite confirmation. 7. Prefer atomic writes through a temporary file in the destination directory followed by `os.replace()`. 8. Reject symlink destinations or use platform-supported no-follow semantics. 9. Apply the same containment and overwrite controls to downloaded result files. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/zcm_lib/files.py:23
Finding
Document Upload Validation Accepts Any Regular Local File<![CDATA[ ## Vulnerability Details **File Location**: `scripts/zcm_lib/files.py:23-45` and `scripts/zcm_lib/files.py:49-71` **Additional Locations**: `scripts/zcm_lib/cli.py:347-352`, `scripts/zcm_lib/cli.py:482-495` **Vulnerability Type**: Insufficient file-type validation before remote upload **Risk Level**: Medium ### Vulnerable Code Snippet ```python def check_size(path, max_mb, label): """Validate a local file size, leaving unreadable files to server-side checks.""" try: size = os.path.getsize(path) except OSError: return if size > max_mb * 1024 * 1024: _die( f"{label}超过大小上限:{size / 1024 / 1024:.1f} MB > {max_mb} MB" f"({os.path.basename(path)})。请压缩或拆分后重试。" ) ``` ```python for field_name, filepath in files: if not os.path.isfile(filepath): _die(f"文件不存在:{filepath}") fname = os.path.basename(filepath) ctype = mimetypes.guess_type(fname)[0] or "application/octet-stream" with open(filepath, "rb") as f: content = f.read() buf += b"--" + boundary.encode() + crlf buf += ( f'Content-Disposition: form-data; name="{field_name}"; filename="{fname}"' ).encode("utf-8") + crlf buf += f"Content-Type: {ctype}".encode() + crlf + crlf buf += content + crlf ``` ### Technical Analysis The documentation states that tender and bid inputs are limited to PDF, DOC, and DOCX documents. The implementation checks only that the path is a regular file and that its size is within the configured limit. `mimetypes.guess_type()` is used only to populate the multipart header; it does not enforce an allowlist or inspect file contents. A file with any extension, or a renamed sensitive file, is read completely and uploaded to the remote service. `check_size()` also returns silently when `os.path.getsize()` fails, deferring validation rather than failing closed. ### Attack Path 1. A non-document local path is mistakenly or maliciously supplied as a tender ...[truncated 739 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce a case-insensitive extension allowlist containing only `.pdf`, `.doc`, and `.docx`. 2. Validate file signatures rather than trusting the extension or MIME guess: - Confirm the PDF signature. - Validate DOC compound-file structure. - Validate DOCX as an expected ZIP/Office container. 3. Fail closed when metadata or readability checks raise an error. 4. Resolve and display the canonical file path for confirmation before upload. 5. Consider rejecting symbolic links or explicitly resolving and reconfirming their targets. 6. Verify that the path corresponds to a file the user explicitly supplied for the current task. 7. Stream large files rather than reading up to multiple gigabytes into memory. 8. Add tests proving that unsupported extensions and renamed arbitrary files are rejected. ]]>

T01 · Skill Instruction Hijacking

Warning
Location
scripts/zcm_lib/cli.py:120
Finding
Error Guidance Solicits a Phone Number Despite the Declared Privacy Boundary<![CDATA[ ## Vulnerability Details **File Location**: `scripts/zcm_lib/cli.py:120` **Additional Location**: `SKILL.md:160` **Vulnerability Type**: Contradictory instruction encouraging collection of personal information **Risk Level**: Medium ### Vulnerable Code Snippet ```python "skill_trial_registration_disabled": "需要先配置百炼标书 Api Key 才能继续使用。你可以登录官网 https://biaoshu.zhiliaobiaoxun.com 注册并获取 Api Key;也可以直接把手机号发给我,由我帮你完成注册和配置。", ``` The Skill's declared privacy boundary states that it does not collect phone numbers or register accounts: ```markdown - **不采集**:本 skill 不采集设备信息、不代注册账号、不收集手机号/验证码。 ``` ### Technical Analysis The error mapping instructs the recipient that they can send a phone number to the Agent so that the Agent can register and configure the account. This directly contradicts the Skill's explicit statement that it does not collect phone numbers, verification codes, or perform registration. The parser's registration and trial commands are disabled in this build because `include_skillhub_auth` defaults to `False`. Therefore, the guidance not only solicits personal data but also advertises an unavailable capability. If the server returns the `skill_trial_registration_disabled` error code and the Agent relays the mapped message, a user may disclose a phone number in chat. ### Attack Path 1. The remote API returns the `skill_trial_registration_disabled` error code. 2. The CLI maps the error to the contradictory guidance. 3. The script prints the message or the Agent relays it. 4. The user follows the instruction and sends a phone number in the conversation. 5. The phone number is retained in chat history even though this build cannot safely complete the promised registration workflow. ### Impact Assessment The issue can lead to unnecessary collection and retention of personally identifiable information. Potential consequences include: - Phone-number exposure in conversation logs, screenshots, or support records. - Loss of user trust due to con ...[truncated 315 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the instruction inviting users to send a phone number to the Agent. 2. Replace it with self-service registration guidance that does not request personal data in chat. 3. Ensure all error messages match the privacy and credential rules in `SKILL.md`. 4. Do not mention registration capabilities that are disabled in the current build. 5. Add a test that scans user-facing messages for requests involving phone numbers, verification codes, API keys, or other credentials. 6. Establish one authoritative source for onboarding guidance to prevent contradictory text across code and documentation. 7. If registration support is added in the future, design a separate, consent-based flow that never exposes phone numbers or verification codes to the conversational Agent. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (2)

Intent-Code Divergence

Medium
Confidence
96% confidence
Finding
The registration/help text tells the assistant it may ask the user for a phone number and help complete registration/configuration on the user's behalf. That undermines the surrounding security guidance that credentials should be created and entered only by the user, and it can normalize collection of identity data and delegated account setup through the chat channel. In this skill context, the risk is elevated because the workflow revolves around a paid API credential that grants account access and billing usage.

Ssd 3

Medium
Confidence
97% confidence
Finding
The error/help copy explicitly encourages the assistant to collect a user's phone number so it can perform account registration/configuration for them. This creates a social-engineering and privacy risk path: phone numbers, verification flows, and eventual credential setup may be mediated by the assistant rather than controlled by the user. Because this skill processes procurement documents and uses a billable API key, encouraging out-of-band identity handling is more dangerous than generic support text.

Static analysis

No suspicious patterns detected.