Back to skill

Security audit

unisound-health-record-management

Security checks for vulnerabilities and agentic risk

Overview

This healthcare extractor is not malware, but it can send full medical records and an API key to a configurable remote LLM without enforcing de-identification or endpoint limits.

Review before installing. Use only with de-identified records, approved medical-data processing terms for the LLM backend, a trusted HTTPS endpoint, and a controlled output location. Avoid passing real patient identifiers or long-lived API keys until the skill enforces redaction and endpoint allowlisting.

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/run.py:29
Finding
Unredacted Health and Identity Data Is Transmitted to an External LLM Service<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:29-35`, `scripts/run.py:170-180`, and `scripts/run.py:244-257` **Vulnerability Type**: Sensitive-data exposure caused by missing local de-identification **Risk Level**: High ### Vulnerable Code ```python def _http_post(url: str, payload: Dict[str, Any], headers: Dict[str, str], *, timeout: int = 0) -> Any: data = json.dumps(payload, ensure_ascii=False).encode("utf-8") req = urllib.request.Request( url=url, data=data, method="POST", headers={"Content-Type": "application/json", **headers}, ) try: ctx = urllib.request.urlopen(req) if not timeout else urllib.request.urlopen(req, timeout=timeout) ``` ```python def run_health_record(health_info: str, llm, output_path: str = "") -> int: prompt = f"""Please extract a structured resident health record from the following resident health information. [Resident health information] {health_info.strip()} Return JSON and a summary according to the required format. Use null for information that was not provided and do not fabricate information.""" print("Extracting resident health record information...") result = llm([sys_msg(SYSTEM_PROMPT), user_msg(prompt)]) ``` ```python try: health_info = load_input(input_path, args.encoding) except Exception as e: print(f"Failed to read input file: {e}", file=sys.stderr) return 1 llm = make_llm_caller(args.appkey, args.base, args.model, args.timeout) try: return run_health_record(health_info, llm, args.output) ``` ### Technical Analysis The application loads the complete contents of the supplied health-record file and embeds them directly into an LLM request. It does not locally detect, redact, tokenize, or reject direct identifiers such as names, government identification numbers, telephone numbers, or addresses. This conflicts with the privacy statements in `SKILL.md`, which state that direct identifiers are not processed and that strict de- ...[truncated 1384 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Implement local identifier detection and redaction before constructing the LLM request. 2. Cover common identifier classes, including names, identity numbers, telephone numbers, email addresses, addresses, insurance identifiers, and patient record numbers. 3. Replace identifiers with stable, non-reversible placeholders when correlation within one record is necessary. 4. Reject or require explicit confirmation for records that cannot be reliably de-identified. 5. Show the operator the redacted form that will be transmitted. 6. Add automated tests verifying that representative identifiers never reach the HTTP payload. 7. Update `SKILL.md` so its privacy claims accurately describe controls enforced by the implementation. 8. Establish retention, access-control, and data-processing requirements for the remote model service. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/run.py:45
Finding
Arbitrary LLM Endpoint Override Can Expose the API Credential and Medical Records<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:45-52`, `scripts/run.py:232-233`, and `scripts/run.py:255` **Vulnerability Type**: Unrestricted security-sensitive endpoint configuration **Risk Level**: High ### Vulnerable Code ```python def make_llm_caller(appkey: str, base: str = DEFAULT_LLM_BASE, model: str = DEFAULT_LLM_MODEL, timeout: int = 0): 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) ``` ```python parser.add_argument("--appkey", required=True, help="Internal medical LLM authentication key assigned by the platform.") parser.add_argument("--base", default=DEFAULT_LLM_BASE, help=f"Internal LLM base URL (default: {DEFAULT_LLM_BASE}).") ``` ```python llm = make_llm_caller(args.appkey, args.base, args.model, args.timeout) ``` ### Technical Analysis The `--base` option accepts an arbitrary URL. The application appends `/chat/completions` and sends both the Bearer credential and the complete health-record prompt to the resulting endpoint. There is no trusted-host allowlist, explicit HTTPS-only policy, endpoint identity validation, or check that the configured destination belongs to the intended medical model provider. Consequently, anyone able to influence the invocation arguments can redirect security-sensitive requests to an endpoint they control. The default endpoint uses HTTPS, but that protection is bypassed if a malicious or erroneous value is supplied through `--base`. ### Attack Path 1. An attacker persuades an operator to use a modified command, wrapper, or configuration containing `--base` with an attacker-controlled URL. 2. The application constructs the chat-completions URL from that untrusted base value. 3. It adds the platform API key to the `Authorization: Bearer` heade ...[truncated 826 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the endpoint override unless it is operationally required. 2. If configurability is required, enforce an allowlist of approved HTTPS schemes, hostnames, and ports. 3. Resolve and validate the final destination before attaching credentials. 4. Reject URLs containing user information, unexpected ports, fragments, or non-HTTPS schemes. 5. Disable automatic cross-origin redirects for authenticated requests, or strip credentials and require revalidation before following one. 6. Store the API key in a protected environment variable or secret manager rather than requiring it on the command line, where it may be visible in shell history or process listings. 7. Use narrowly scoped, short-lived credentials and support immediate revocation and rotation. 8. Log only destination metadata and never log the authorization header or medical request body. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/run.py:170
Finding
Untrusted Clinical Text Can Manipulate Generated Health Records Through Prompt Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:170-180` **Vulnerability Type**: Indirect prompt injection and insufficient output validation **Risk Level**: Medium ### Vulnerable Code ```python def run_health_record(health_info: str, llm, output_path: str = "") -> int: prompt = f"""Please extract a structured resident health record from the following resident health information. [Resident health information] {health_info.strip()} Return JSON and a summary according to the required format. Use null for information that was not provided and do not fabricate information.""" print("Extracting resident health record information...") result = llm([sys_msg(SYSTEM_PROMPT), user_msg(prompt)]) ``` The returned content is only checked for basic JSON parseability: ```python parsed_json = None try: if "```" in json_part: start = json_part.find("{") end = json_part.rfind("}") + 1 json_part = json_part[start:end] parsed_json = json.loads(json_part) except json.JSONDecodeError: print("Warning: model JSON output could not be parsed; preserving raw output.", file=sys.stderr) ``` ### Technical Analysis The application directly interpolates untrusted clinical text into a model message that also contains task instructions. It does not tell the model to treat instructions found inside the record as inert data, and it does not delimit or encode the record in a way that provides robust instruction separation. An input record can therefore contain text instructing the model to ignore extraction rules, fabricate diagnoses, omit risk factors, or produce attacker-selected follow-up recommendations. Basic `json.loads` validation only establishes syntactic validity. It does not validate the expected schema, permitted types, medically plausible values, or whether output claims are supported by the source record. The documentation requires human review, which reduces operational risk when followed, but the implem ...[truncated 1134 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Explicitly state in the system prompt that record content is untrusted data and that any instructions appearing within it must never be followed. 2. Place the record in a clearly delimited data block and use an API-supported structured-output or JSON-schema mode when available. 3. Validate the output against a strict local schema, including required keys, data types, enumerated values, and numeric ranges. 4. Reject unknown fields and malformed structures rather than preserving arbitrary raw model output. 5. Require evidence mapping so each extracted fact references supporting source text. 6. Flag unsupported diagnoses, medication changes, and follow-up recommendations for human review. 7. Add adversarial tests containing common prompt-injection patterns. 8. Ensure downstream systems treat generated content as untrusted and do not automatically commit it to an authoritative health-record system. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (4)

Missing User Warnings

High
Confidence
95% confidence
Finding
The skill sends raw resident health-record content to a remote LLM API, but the CLI and runtime flow do not present a clear user-facing warning, consent gate, or data-handling notice before transmitting highly sensitive medical information. In a healthcare context, this can expose protected health information to external infrastructure, logging systems, retention pipelines, or cross-border processing without the operator fully understanding that transmission is occurring.

Ssd 3

High
Confidence
96% confidence
Finding
The code forwards user-supplied health records in full to the LLM and then outputs the extracted JSON and summary, creating a direct sensitive-data exposure path for medical details. Because the skill processes resident health archives, the data is especially sensitive; full-fidelity transmission and reproduction increase the chance of disclosure through the model provider, terminal output, saved files, logs, or downstream consumers.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises file read, file write, and network-capable execution paths via its documented runner, but does not declare any explicit tool scope or permissions boundary in the skill manifest. In a healthcare context handling sensitive medical records and an appkey for an internal model API, this lack of least-privilege declaration increases the risk of unintended data exposure, overbroad execution, or misuse by an agent runtime that relies on manifest-scoped controls.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The description specifies the output format as "JSON + 自然语言摘要" in Chinese, and the document consistently presents the summary format in Chinese without any opt-in or alternative language option. Under the policy rule, forcing a specific language is a natural-language policy violation unless the locale restriction is explicitly justified as region-specific.

Static analysis

No suspicious patterns detected.