Back to skill

Security audit

unisound-chief-complaint-diagnosis-inconsistent

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherent for medical-record quality checks, but it handles sensitive medical text and credentials with under-scoped network and persistence controls.

Review this skill before installing in any clinical or regulated environment. Use only de-identified records, restrict the LLM endpoint to an approved HTTPS host, avoid --save-prepared unless local plaintext storage is approved, and treat results as assistant QC output requiring 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:58
Finding
Unrestricted LLM Endpoint Can Exfiltrate API Credentials and Medical Records<![CDATA[ ## Vulnerability Details **File Location**: `scripts/emr_qc_impl.py:58-65` **Additional Locations**: `scripts/emr_qc.py:27-30`, `scripts/run.py:54-57` **Vulnerability Type**: Unrestricted destination for sensitive outbound requests **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): """Return an llm(messages) → str calling function.""" 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 endpoint is exposed through an unrestricted command-line argument: ```python parser.add_argument( "--base", default=DEFAULT_LLM_BASE, help=f"LLM base URL (default: {DEFAULT_LLM_BASE}).", ) ``` ### Technical Analysis The `--base` parameter accepts an arbitrary URL. The application appends `/chat/completions` and sends both of the following to that destination: - The application key in the `Authorization: Bearer` header. - EMR-derived content in the request payload. No destination hostname allowlist is applied. The implementation also does not explicitly reject plaintext HTTP URLs, embedded URL credentials, loopback destinations, private network addresses, or redirects to untrusted destinations. Although custom endpoint support may be intentional, using the same sensitive application credential for every caller-selected destination crosses a security boundary. Any party capable of influencing the command-line invocation or wrapper configuration can redirect credentials and medical data to infrastructure under its control. ### Attack Path 1. An attacker gains influence over the Skill invocation, a wrapper script, job configuration, or copied command. 2. The attacker supplies a destination ...[truncated 1214 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Allowlist the exact approved API hostname and scheme. 2. Reject non-HTTPS URLs, embedded credentials, fragments, and unexpected ports. 3. Resolve and reject loopback, link-local, and private network destinations unless explicitly required. 4. Disable automatic cross-origin redirects or validate the destination again after every redirect. 5. Do not transmit the production application key to a caller-selected endpoint. 6. If custom providers are required, use separately supplied credentials scoped to each provider. 7. Require an explicit high-visibility confirmation before transmitting medical data to a non-default service. 8. Log only the approved destination hostname; never log authorization headers or medical payloads. 9. Add tests confirming that malformed URLs, HTTP URLs, unapproved hosts, and redirect-based bypasses are rejected. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/emr_qc_impl.py:130
Finding
Medical Record Content Can Inject Instructions into LLM Quality-Control Prompts<![CDATA[ ## Vulnerability Details **File Location**: `scripts/emr_qc_impl.py:130-158` **Additional Locations**: `scripts/emr_qc_impl.py:165-213`, `scripts/emr_qc_impl.py:220-266` **Vulnerability Type**: Indirect prompt injection through untrusted EMR fields **Risk Level**: Medium ### Vulnerable Code The complaint value is directly inserted into an instruction-bearing prompt: ```python cc_has_loc = llm([ sys_msg("You are an outpatient medical-record quality-control assistant. Follow the user's output requirements strictly."), user_msg(f"""Given the chief-complaint field, determine whether it contains a body-location description and answer only "contains" or "does not contain". Evaluate the following complaint: {cc}""" ), ]) if "does not contain" in cc_has_loc: return "no defect" ``` The diagnosis and final quality-control stages use the same direct interpolation pattern: ```python Evaluate the following preliminary diagnosis: {dx} ``` ```python Now evaluate the following chief complaint and preliminary diagnosis: Chief complaint: {cc} Diagnosis: {dx} ``` ### Technical Analysis The values `cc` and `dx` originate from an input medical record and are treated as trusted prompt content. They are interpolated into the same natural-language message that contains operational instructions for the model. There is no robust separation between instructions and data, no explicit warning that directives appearing inside the medical record must be ignored, and no structured response validation. The code accepts model output through substring checks such as checking whether a phrase equivalent to `does not contain` appears anywhere in the response. Consequently, a crafted complaint or diagnosis can contain instructions directing the model to emit the branch-controlling phrase. Because the check is based on substring presence rather than an exact enumerated response, the attack does not necessarily require full control o ...[truncated 1626 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Place untrusted record values in a clearly delimited structured object, such as JSON with dedicated `chief_complaint` and `diagnosis` properties. 2. Add a high-priority instruction stating that all content inside record fields is untrusted data and that directives contained in those fields must never be followed. 3. Use a strict machine-readable response schema, for example: ```json {"contains_location": true} ``` 4. Parse the response as JSON and require exact types and allowed values; reject additional text. 5. Replace substring checks with exact comparisons against validated enum values. 6. Validate the final result against a strict schema before writing it to disk or using it downstream. 7. Treat parse or schema failures as indeterminate results requiring manual review rather than as no-defect decisions. 8. Add adversarial tests containing instruction-like text, fake examples, role markers, and branch-controlling phrases in every user-controlled record field. 9. Where practical, supplement LLM decisions with deterministic extraction of laterality and anatomical-location terms. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/run.py:102
Finding
Optional Debug Mode Persists Complete Medical Records in Plaintext<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:102-106` **Documentation Location**: `SKILL.md:22-23` **Vulnerability Type**: Plaintext persistence of sensitive medical data **Risk Level**: Medium ### Vulnerable Code ```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") ``` The Skill documentation states that inputs and intermediate results are not persisted locally and are destroyed after the call. The `--save-prepared` behavior contradicts that broad privacy assurance by writing the complete preprocessed record to an ordinary plaintext file. ### Technical Analysis When `--save-prepared` is enabled, `record_text` is saved without redaction or field minimization. The implementation uses normal filesystem creation semantics and does not explicitly set restrictive permissions. The resulting file can remain after processing and may be included in backups, disk snapshots, support bundles, artifact collections, or later repository operations. Its actual accessibility depends on the process umask, parent-directory permissions, and host configuration. Because preprocessing can extract content from PDF, Word, Excel, CSV, text, and JSON inputs, the persisted file may contain substantially more information than the complaint and diagnosis fields required by this quality-control rule. ### Attack Path 1. A user or automation process invokes `scripts/run.py` with `--save-prepared`. 2. The preprocessor extracts text from the supplied medical document. 3. The complete extracted text is assigned to `record_text`. 4. The Skill writes that value to a predictable `.prepared.txt` path. 5. The file remains on disk after the process terminates. 6. Another local user, service, backup agent, artifact col ...[truncated 727 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `--save-prepared` from production builds unless it is operationally necessary. 2. Clearly document that enabling the option persists potentially sensitive medical data. 3. Require an explicit acknowledgement before saving unredacted content. 4. Minimize saved data to the complaint and diagnosis fields required by this rule. 5. Redact direct identifiers and other unrelated medical information before persistence. 6. Create the output atomically with owner-only permissions, such as mode `0600`, rather than relying solely on the ambient umask. 7. Store diagnostic files only in a protected, dedicated directory with restrictive permissions. 8. Implement a short retention policy and reliable deletion process. 9. Prevent prepared files from entering source control, routine artifacts, logs, and backups where possible. 10. Update the privacy statement so it accurately distinguishes default behavior from optional persistence. ]]>
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 (9)

Missing User Warnings

High
Confidence
97% confidence
Finding
This skill sends chief complaint and diagnosis data derived from outpatient medical records to an external LLM service endpoint, which can expose sensitive health information to a third party. In a medical context, transmitting record content off-box without explicit disclosure, minimization, or consent creates serious confidentiality and compliance risk even if the endpoint is nominally 'internal' or trusted.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill documentation declares no explicit tool scope or permission boundaries, yet the skill clearly supports file reading, file writing, and outbound network access via local file inputs, output file generation, and calls to an external MaaS endpoint. Without an allowlist or declared restrictions, a hosting agent may grant broader capabilities than intended, increasing the risk of unauthorized data access or exfiltration, especially because the input is medical record content.

Ssd 1

Medium
Confidence
92% confidence
Finding
User-controlled medical text is interpolated directly into prompts that instruct the model to follow formatting rules, so crafted record content can semantically override or confuse the classifier. This can cause the model to emit incorrect safety-critical QC judgments, which is especially concerning in a medical quality-control skill where downstream users may rely on the result.

Ssd 1

Medium
Confidence
94% confidence
Finding
The final decision prompt embeds raw chief complaint and diagnosis text into a single free-form instruction, allowing crafted input to manipulate the model's reasoning or output format. Because this stage produces the ultimate defect/no-defect decision and rationale, prompt injection here can directly corrupt clinical QC outcomes and reduce trustworthiness of the tool.

Description-Behavior Mismatch

Medium
Confidence
90% confidence
Finding
The manifest describes a text-in/text-out QC capability: given outpatient record text, call an internal medical LLM and output whether there is a defect and why. In addition to producing that result, the code creates directories and persists the result to ../runs/med-emr-qc or a caller-supplied path, which is a broader behavior than the stated description.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The QC result is written to disk by default without clear disclosure, creating unintended persistence of potentially sensitive medical assessment output. In healthcare workflows, even derived results can contain patient-linked clinical information, increasing the risk of local data exposure, retention beyond policy, or collection by other processes/users on the host.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
When --save-prepared is used, the script writes preprocessed medical text directly to disk without any warning about storing sensitive health information locally. This can expose PHI through insecure local files, backups, shared directories, or later forensic recovery, especially in multi-user or managed environments.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script sends outpatient medical record text to an LLM service via run_qc without any explicit consent prompt, masking, or user-facing warning that sensitive health data will be transmitted off-process or potentially off-host. In a medical QC skill, this creates a real privacy and compliance risk because the input can contain protected health information and the base URL is even user-configurable, increasing the chance of disclosure to an unintended endpoint.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
This code file contains user-facing natural-language strings entirely in Chinese, including the module docstring and CLI help text. Under the policy for natural-language violations, forcing a specific language without user opt-in can be a locale/language policy issue when no alternative language choice is offered.

Static analysis

No suspicious patterns detected.