Back to skill

Security audit

unisound-followup-record

Security checks for vulnerabilities and agentic risk

Overview

This skill performs a coherent medical-record extraction task, but its privacy and persistence guarantees do not match the code handling sensitive medical records.

Review carefully before installing or using with real patient records. Treat inputs and outputs as sensitive, do not rely on the stated de-identification or no-persistence guarantees, use only approved HTTPS model endpoints, avoid --save-prepared unless retention is intended, and require human review of extracted clinical fields.

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)

other

Error
Location
scripts/run.py:184
Finding
Medical records are transmitted without the promised de-identification<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:184-201`, `scripts/run.py:218-244`, and `scripts/run.py:328-345` **Vulnerability Type**: Sensitive Medical Data Disclosure **Risk Level**: High ### Technical Analysis The documentation at `SKILL.md:42` states that personally identifiable information will be de-identified before any model or API request. The implementation does not perform such de-identification. Instead, the complete record is inserted directly into a prompt and transmitted to the configured LLM endpoint. Relevant code from `scripts/run.py:184-201`: ```python def build_prompt(payload: dict[str, Any]) -> tuple[str, str]: """构建 LLM 提示词,返回 (part1_prompt, part2_prompt).""" record = payload.get("record") or payload.get("text") or payload.get("content") or "" if not record.strip(): raise ValueError("输入缺少 record 字段") # 第一步:分块 - 将病历分为"患者的情况"和"医生的处理意见" chunk_prompt = """给定下面的病历文本,请抽取出两部分 1.患者的情况 2.医生的处理意见 输入: {} 输出: """.strip().format(record) return chunk_prompt, None ``` The derived medical content is subsequently transmitted two more times: ```python def run( payload: dict[str, Any], *, base: str, model: str, appkey: str, timeout: int ) -> str: """执行复诊病历生成.""" chunk_prompt, _ = build_prompt(payload) # 第一步:分块 chunk_result = call_llm(chunk_prompt, base=base, model=model, appkey=appkey, timeout=timeout) # 提取两部分内容 record1, record2 = extract_chunk_result(chunk_result) # 第二步:分别抽取 part1_prompt, part2_prompt = build_extract_prompts(record1, record2) part1_result = call_llm(part1_prompt, base=base, model=model, appkey=appkey, timeout=timeout) part2_result = call_llm(part2_prompt, base=base, model=model, appkey=appkey, timeout=timeout) ``` There is no redaction stage for names, government identifiers, telephone numbers, addresses, or other identifying information. Consequently, records containing personally identifiable health informat ...[truncated 1118 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Introduce a mandatory de-identification stage before constructing any prompt. 2. Detect and redact, tokenize, or pseudonymize names, identity numbers, telephone numbers, email addresses, detailed addresses, medical record identifiers, and other direct identifiers. 3. Reject transmission when identifiers cannot be handled with sufficient confidence, or require explicit informed authorization through the calling application. 4. Minimize the transmitted content to only the fields necessary for extraction. 5. Add automated tests proving that representative identifiers never appear in outgoing HTTP request bodies. 6. Consider local preprocessing or a locally hosted model for records that cannot be safely de-identified. 7. Document the actual processing behavior, residual re-identification risks, service operator, retention policy, and deletion guarantees. 8. Ensure that optional prepared-data and output files have restrictive permissions and an explicit retention policy. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/run.py:165
Finding
Unrestricted API base URL can disclose the bearer credential and medical records<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:165-175` and `scripts/run.py:380-384` **Vulnerability Type**: Unrestricted Credential-Bearing Network Destination **Risk Level**: High ### Technical Analysis The caller can supply an arbitrary API base URL through `--base`. The code appends `/chat/completions` and sends both the bearer credential and prompt body to that destination. Relevant HTTP request construction from `scripts/run.py:165-175`: ```python def call_llm(prompt: str, *, base: str, model: str, appkey: str, timeout: int) -> str: """调用内部医疗大模型.""" url = f"{base.rstrip('/')}/chat/completions" headers = {"Authorization": f"Bearer {appkey}"} if appkey else {} payload = { "model": model, "messages": [{"role": "user", "content": prompt}], "temperature": 0, } response = _http_post(url, payload, headers, timeout=timeout) ``` The base URL is exposed as an unrestricted command-line argument: ```python parser.add_argument( "--base", default=DEFAULT_LLM_BASE, help=f"内部大模型 base URL(默认:{DEFAULT_LLM_BASE}).", ) ``` The implementation does not enforce HTTPS, validate the destination hostname against a trusted allowlist, or prevent the credential from being sent to an untrusted origin. If an attacker can influence invocation arguments, a malicious endpoint can collect both the API key and sensitive record content. An `http://` destination can additionally expose both values in cleartext over the network. ### Attack Path 1. An attacker influences the command invocation, wrapper configuration, deployment configuration, or user instructions. 2. The attacker sets `--base` to an endpoint under their control, such as `https://attacker.example/v1`. 3. The application constructs `https://attacker.example/v1/chat/completions`. 4. `_http_post()` sends the `Authorization: Bearer ...` header and the medical-record prompt to the attacker-controlled server. 5. The attacker captures the crede ...[truncated 795 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer a fixed API endpoint compiled into trusted configuration rather than accepting an arbitrary command-line URL. 2. If endpoint overrides are required, allowlist exact trusted HTTPS origins and ports. 3. Parse and canonicalize the URL before validation; reject user-info components, fragments, unexpected ports, IP literals, and malformed hostnames. 4. Reject all non-HTTPS schemes. 5. Disable redirects or verify every redirect target before forwarding credentials. 6. Bind credentials to a specific origin and never attach the bearer header to any other destination. 7. Store the credential in a protected secret provider or environment variable rather than routinely exposing it in process command lines. 8. Use narrowly scoped, short-lived keys and rotate any key that may have been sent to an untrusted endpoint. 9. Add tests confirming that unapproved destinations and plaintext HTTP URLs are rejected before any network request occurs. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/run.py:218
Finding
Untrusted medical-record content is directly embedded into LLM instructions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:218-244` **Vulnerability Type**: Indirect Prompt Injection **Risk Level**: Medium ### Technical Analysis Model-generated or user-controlled record content is concatenated directly into extraction prompts. The prompts do not clearly isolate the record as untrusted data, instruct the model to ignore directives contained in that data, or enforce a machine-validated output schema. Relevant code: ```python def build_extract_prompts(record1: str, record2: str) -> tuple[str, str]: """构建抽取提示词.""" # 第一部分:病史相关 part1_prompt = f""" 给定患者病历中病史相关内容,请按要求抽取出其中对应的部分 1.基本原则:忠实于原文,只做拆解,不要加工 2.抽取以下字段,如果没有对应内容,请回答"未提及" {chr(10).join(FIELDS_PART1)} 病史相关内容如下: {record1} """.strip() # 第二部分:处理意见相关 part2_prompt = f""" 给定患者病历中处理意见和随诊相关内容,请按要求抽取出其中对应的部分 1.基本原则:忠实于原文,只做拆解,不要加工 2."本人就诊"不用抽取 3.抽取以下字段,如果没有对应内容,请回答"未提及" {chr(10).join(FIELDS_PART2)} 处理意见相关内容如下: {record2} """.strip() return part1_prompt, part2_prompt ``` A crafted medical record can contain instructions directing the model to ignore the extraction task, fabricate fields, omit information, or emit attacker-selected output. The first model call also processes the untrusted record and its output is then reused in subsequent prompts, allowing malicious instructions to propagate through the multi-stage pipeline. The final response is handled as free-form text. `postprocess_result()` normalizes selected lines but does not enforce an exact field allowlist, reject unexpected content, or verify that values are grounded in the source record. ### Attack Path 1. An attacker places prompt-like instructions inside a medical record or an imported document. 2. The complete record is passed to the first LLM request. 3. The first model may preserve or amplify the embedded directives in `record1` or `record2`. 4. `build_extract_prompts()` inserts that content directly after the extraction instructions. 5. The model follows the embedded direct ...[truncated 892 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Treat all document and record content as untrusted data. 2. Use separate message roles where supported, placing stable extraction policy in a trusted system or developer message and the record in a clearly identified data field. 3. Delimit the record with unambiguous boundaries and explicitly instruct the model never to follow instructions found inside those boundaries. 4. Prefer a structured-output or function-calling interface with a fixed JSON schema. 5. Validate that the response contains exactly the permitted fields and expected value types. 6. Reject unexpected prose, extra fields, executable content, or malformed responses rather than printing them. 7. Verify extracted values against the source text where feasible and flag unsupported values for review. 8. Add adversarial tests containing instructions such as requests to ignore prior rules, fabricate diagnoses, or change the output format. 9. Require human review before using extracted values in clinical or treatment-related workflows. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (11)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The documented behavior does not match the stated purpose: it claims JSON structuring but describes line-based text output, remote transmission to an LLM API, and optional local file persistence. This mismatch can mislead users and reviewers about what data leaves the environment and how it is handled, increasing the risk of improper use with sensitive medical information.

Intent-Code Divergence

High
Confidence
98% confidence
Finding
The documentation states there is no local persistence and that data is destroyed after the call, yet the interface includes options to save output JSON, output text, and prepared intermediate text to disk. In a healthcare setting, such contradictory guarantees can cause accidental retention of sensitive patient data and compliance violations.

Missing User Warnings

High
Confidence
97% confidence
Finding
The code sends raw medical record contents to a remote LLM API via `messages` without any user-facing disclosure, consent, minimization, or execution-path warning. Because the data is medical and therefore highly sensitive, silent transmission to an external service can cause serious privacy, compliance, and data-governance violations even if the endpoint is described as internal.

Lp3

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding
The skill documentation exposes capabilities to read files, write outputs, and make network requests, but it does not declare any tool scope or permissions boundary. In a medical-records context, undeclared capability expansion is dangerous because sensitive patient data may be transmitted or persisted without clear operator awareness or policy enforcement.

Intent-Code Divergence

Medium
Confidence
90% confidence
Finding
The skill promises strict de-identification before any model or API transmission, but the documented workflow does not describe a concrete de-identification step before sending records to the remote LLM. For clinical follow-up notes, this can result in direct exposure of personal health information if operators rely on the privacy claim and submit raw records.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
模块顶层文档字符串明确写明“抽取并规范化为细粒度字段 JSON”,但实现中的提示词、后处理和输出路径都围绕换行分隔的 `字段:值` 文本展开,没有实际 JSON 规范化步骤。这里不是文档信息缺失,而是文档对最终产物的描述与代码行为相矛盾。

Natural-Language Policy Violations

Medium
Confidence
86% confidence
Finding
Natural-language descriptions, prompts, and CLI help text are written exclusively in Chinese, and the skill provides no language-selection mechanism or documented opt-in for that locale. Under the stated policy, forcing a specific language without user choice is a natural-language policy violation.

Ssd 1

Medium
Confidence
95% confidence
Finding
Untrusted medical record text is interpolated directly into the prompt with no delimiting, quoting, or instruction-hierarchy safeguard, so content inside the record can masquerade as instructions to the model. In this medical context, prompt injection can degrade extraction integrity, cause omission or fabrication of clinical fields, or steer the model into disclosing/transcribing unintended content.

Ssd 1

Medium
Confidence
97% confidence
Finding
The workflow feeds first-stage model output (`record1`, `record2`) back into new prompts, allowing any injected instructions or manipulated content that survives stage one to influence subsequent extraction calls. This amplification across multiple LLM steps is especially risky for medical record processing because it can systematically corrupt structured clinical outputs and make tampering harder to detect.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
清单描述该技能应将门诊复诊病历“结构化为细粒度字段,输出 JSON”,但主流程 `run()` 仅拼接两次 LLM 返回的文本并做字符串后处理,`main()` 也直接将该文本写出或打印,没有将结果解析/组装为 JSON 对象。该行为与技能对外宣称的输出形态存在直接语义不一致。

Natural-Language Policy Violations

Low
Confidence
82% confidence
Finding
The JSON value is entirely in Chinese and provides a fixed natural-language record template/content without indicating that language selection is optional. Under the policy rule, forcing a specific language without user opt-in can be a locale-policy violation unless the constraint is explicitly justified.

Static analysis

No suspicious patterns detected.