Back to skill

Security audit

unisound-followup-reminder

Security checks for vulnerabilities and agentic risk

Overview

The skill broadly matches a patient follow-up reminder, but it handles sensitive medical files and sends patient data to a remote model without enough controls, and it can load unaudited code from outside the skill.

Review before installing in any real patient or regulated healthcare setting. Use only non-sensitive test data unless the remote model provider, retention terms, consent process, and data-minimization requirements are approved. Prefer JSON-only inputs, avoid free-form notes with identifiers, avoid --save-prepared for real records, and remove or verify the external _shared preprocessing fallback before deployment.

Vulnerability Patterns
  • Tool Hijacking and SpoofingModifies or replaces tools so legitimate-looking calls execute attacker logic
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/run.py:95
Finding
Prompt Injection Through Untrusted Patient Data<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py`, lines 95–108 **Vulnerability Type**: Prompt injection caused by directly interpolating untrusted data into an LLM prompt **Risk Level**: Medium ### Vulnerable Code ```python user_prompt = f"""请为以下患者生成复诊提醒: 疾病类型:{disease_type} 上次就诊:{last_visit_date_str} 复诊日期:{followup_date.isoformat()} 当前日期:{today.isoformat()} 是否逾期:{'是,已逾期' + str(days_overdue) + '天' if is_overdue else '否,还有' + str(-days_overdue) + '天'} 备注:{note} 请分析风险并给出复诊建议和准备清单。""" text = _call_llm(SYSTEM_PROMPT, user_prompt, appkey) ``` ### Technical Analysis The application inserts the attacker-controllable `disease_type`, `last_visit_date_str`, and `note` values directly into an instruction sent to the language model. These values are not isolated as untrusted data, constrained by length, or validated for instruction-like content. An attacker can place additional model instructions in the `note` field, such as directions to ignore the intended task, generate misleading medical guidance, include an attacker-controlled link, or omit the required disclaimer. Because the generated response is accepted as an unrestricted string and returned in the `text` field, the application has no deterministic mechanism for detecting whether the model followed injected instructions. This issue affects output integrity rather than local operating-system execution. The date calculation remains local and deterministic, but the natural-language medical reminder can be manipulated. ### Attack Path 1. An attacker creates an otherwise valid input document or JSON object. 2. The attacker places adversarial model instructions in `note`, `disease_type`, or another interpolated string field. 3. `build()` embeds that content into `user_prompt` without a trust boundary. 4. `_call_llm()` sends the combined instructions and patient data to the remote model. 5. The model may follow the embedded instructions instead of, or in addition to, the intended reminder ...[truncated 567 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Treat every parsed patient field as untrusted data and place it in a clearly delimited data structure, preferably a JSON object in a separate message. 2. Add an explicit system-level rule stating that content inside patient fields is data and that instructions contained within those fields must never be followed. 3. Validate field types and enforce strict maximum lengths before constructing the request. 4. Ask the model for a constrained JSON response with an explicit schema rather than unrestricted Markdown. 5. Validate the response against that schema and reject unexpected fields, URLs, active content, or missing safety language. 6. Generate critical medical boundaries and disclaimers locally rather than relying on the model to preserve them. 7. Sanitize the final response according to the rendering context, especially if Markdown links or embedded HTML can be displayed. 8. Keep deterministic values such as follow-up dates and overdue status authoritative; do not permit model output to override them. ]]>

T07 · Tool Hijacking and Spoofing

Error
Location
scripts/run.py:221
Finding
Unverified Execution of a Python Module Outside the Audited Skill Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py`, lines 221–230 **Vulnerability Type**: Dynamic execution of an unverified local module **Risk Level**: High ### Vulnerable Code ```python _shared_dir = Path(__file__).resolve().parent.parents[3] / "_shared" / "doc-preprocess" / "scripts" if not _shared_dir.exists(): print(f"ERROR: 无法读取输入文件,本地预处理失败且 _shared/doc-preprocess 不可用。原因:{exc}", file=sys.stderr) return 1 import importlib.util as _iu _spec = _iu.spec_from_file_location("_shared_preprocess", _shared_dir / "preprocess.py") _sp = _iu.module_from_spec(_spec) _spec.loader.exec_module(_sp) input_type = _sp.detect_input_type(input_path, args.input_type) ``` ### Technical Analysis When local preprocessing raises `PreprocessError`, the application constructs a path outside the skill package and executes `_shared/doc-preprocess/scripts/preprocess.py` through `exec_module()`. The code checks only whether the directory exists. It does not verify the module's ownership, resolved location, signature, hash, package version, or permissions. Importing the module executes all of its top-level Python code before any preprocessing function is called. Consequently, any actor who can create or modify the expected shared module can cause arbitrary Python code to run under the identity and privileges of the skill process. The external module was not present in the audited project structure, so its behavior cannot be established by this audit. ### Attack Path 1. An attacker obtains write access to the expected `_shared/doc-preprocess/scripts` path or compromises another component responsible for populating it. 2. The attacker creates or modifies `preprocess.py` and places arbitrary code at module scope. 3. The attacker or a user supplies an input that causes the bundled preprocessing implementation to raise `PreprocessError`. 4. The fallback branch resolves the shared directory and loads the attacker's file. 5. `_spec.loader.exec_module(_sp)` ex ...[truncated 707 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the dynamic parent-directory fallback and use only preprocessing code shipped within the reviewed skill. 2. If code sharing is necessary, distribute the preprocessor as a pinned, installed package from a trusted source. 3. Avoid `spec_from_file_location()` and `exec_module()` for modules discovered through writable or externally managed paths. 4. Verify the resolved path, regular-file status, owner, permissions, and cryptographic digest before loading any external component. 5. Ensure that the shared directory and every parent directory are not writable by untrusted users. 6. Fail closed when trusted preprocessing is unavailable rather than executing an unverified fallback. 7. Run document preprocessing in a restricted subprocess or sandbox with minimal filesystem and network permissions. 8. Correct and test the fallback call signature separately: the existing `build(data, args.appkey)` call does not provide the required `today` argument and will fail if reached. ]]>

other

Error
Location
scripts/run.py:30
Finding
External Disclosure of Patient Health Information Without Data Minimization<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py`, lines 30–39 and 95–108 **Vulnerability Type**: Transmission of sensitive health information to an external API **Risk Level**: High ### Vulnerable Code ```python def _call_llm(system_prompt: str, user_prompt: str, appkey: str) -> str: payload = {"model": MODEL, "temperature": 0.0, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]} try: req = Request(API_URL, data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Content-Type": "application/json", "Authorization": f"Bearer {appkey}"}) resp = urlopen(req, timeout=120) return json.loads(resp.read().decode("utf-8"))["choices"][0]["message"]["content"] ``` ```python user_prompt = f"""请为以下患者生成复诊提醒: 疾病类型:{disease_type} 上次就诊:{last_visit_date_str} 复诊日期:{followup_date.isoformat()} 当前日期:{today.isoformat()} 是否逾期:{'是,已逾期' + str(days_overdue) + '天' if is_overdue else '否,还有' + str(-days_overdue) + '天'} 备注:{note} 请分析风险并给出复诊建议和准备清单。""" text = _call_llm(SYSTEM_PROMPT, user_prompt, appkey) ``` ### Technical Analysis The skill transmits disease type, visit dates, overdue status, and free-form notes to `https://maas-api.hivoice.cn/v1/chat/completions`. These values constitute health-related information, and the free-form note may additionally contain patient names, contact information, identifiers, medication details, diagnoses, or other sensitive records. The endpoint and mandatory API use are documented in `SKILL.md`, so this is not concealed network exfiltration. However, the implementation provides no consent gate, redaction, pseudonymization, field-level minimization, retention warning, or local-only execution mode. Even though HTTPS protects data in transit, it does not address disclosure to, storage by, or subsequent processing by the external service. ### Attack Path 1. A user supplies a patient record as J ...[truncated 1106 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add an explicit consent and disclosure step before transmitting any patient information. 2. Clearly identify the external provider, transmitted fields, processing purpose, retention policy, data residency, and deletion process. 3. Remove or redact direct identifiers and pseudonymize records before constructing the request. 4. Do not send free-form notes by default. Use an allowlist of narrowly defined fields and require explicit user approval for additional content. 5. Provide a local-only mode for deterministic follow-up-date and overdue-status calculations. 6. Apply data-loss-prevention checks to detect names, contact details, government identifiers, record numbers, and other unnecessary sensitive content. 7. Establish appropriate contractual, privacy, security, and regulatory controls with the API provider before processing real patient data. 8. Log only minimal operational metadata and never log prompts, health fields, or bearer tokens. 9. Define retention and deletion controls for both local prepared files and remotely processed prompts. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (19)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared purpose is a narrow follow-up reminder, but the documented behavior expands into general document ingestion, OCR, office conversion, and preprocessing. This mismatch is dangerous because it creates hidden attack surface and can cause users or reviewers to underestimate the amount of sensitive medical data being collected, transformed, and potentially exposed.

Description-Behavior Mismatch

High
Confidence
97% confidence
Finding
The file implements a broad document ingestion and extraction utility that is not aligned with the stated patient follow-up reminder purpose. This capability expansion materially increases attack surface by adding parsing of PDFs, Office files, spreadsheets, JSON, and OCR, including execution of external converters, without clear necessity for the skill's mission.

Missing User Warnings

High
Confidence
96% confidence
Finding
The code sends patient medical context to an external HTTP API without any visible user warning, consent flow, or policy guardrails. Because the transmitted fields can reveal health status and treatment history, this creates a meaningful confidentiality and compliance risk, especially in healthcare workflows.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill advertises broad capabilities including environment access, file read/write, network access, and shell execution, but does not declare any explicit tool scope or permission boundaries. In a medical reminder context, this lack of least-privilege controls increases the chance that a simple reminder skill can access sensitive files, invoke external tools, or transmit data beyond what users would reasonably expect.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The skill accepts unrestricted documents and images, extracts their contents via OCR/parsers, and then generates user-facing output through a remote LLM API. In a patient follow-up setting, that means arbitrary uploaded medical content may be collected and transmitted off-process, greatly increasing confidentiality risk and enabling overcollection far beyond the minimum needed for reminder logic.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The skill explicitly states that API inference is mandatory and that the user's appkey is used to send content to a remote model endpoint, even though the business purpose is a simple reminder/overdue calculation that can be done locally. Forcing remote transmission of patient follow-up data without necessity increases privacy exposure and creates avoidable dependency on an external service for sensitive healthcare processing.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The documentation notes transmission of medical input to an internal model API but does not prominently warn users about privacy, retention, sharing, or consent implications. In a healthcare context, insufficient disclosure can lead to unauthorized handling of sensitive patient information and undermines informed use of the skill.

Natural-Language Policy Violations

Medium
Confidence
78% confidence
Finding
The module docstring is written entirely in Chinese and the OCR logic later prefers Chinese plus English when available, indicating a language-specific default without any visible user choice or justification in this file. Under the policy, forcing a specific language or locale without opt-in is a natural-language policy concern.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The code invokes external office, PDF, and OCR executables even though the skill is described as a patient follow-up/reminder capability. In context, this mismatch makes the behavior more suspicious because it enables processing complex untrusted content and expands system interaction beyond what users or reviewers would reasonably expect from a reminder workflow.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The code silently performs subprocess-based conversion and OCR on user-supplied files without any visible disclosure, warning, or consent mechanism in this component. In a medical context, that can violate user expectations and complicate privacy, audit, and risk review because sensitive patient documents may be handed to external tools unexpectedly.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if not office_bin:
        raise PreprocessError("libreoffice/soffice not found for office document conversion.")
    with tempfile.TemporaryDirectory(prefix="med-skill-preprocess-") as tmp_dir:
        proc = subprocess.run(
            [office_bin, "--headless", "--convert-to", "txt:Text", "--outdir", tmp_dir, str(path)],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if not office_bin:
        raise PreprocessError("libreoffice/soffice not found for xls conversion.")
    with tempfile.TemporaryDirectory(prefix="med-skill-preprocess-") as tmp_dir:
        proc = subprocess.run(
            [office_bin, "--headless",
             "--convert-to", "csv:Text - txt - csv (StarCalc):44,34,76,1",
             "--outdir", tmp_dir, str(path)],
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
pass
    pdf_to_text = shutil_which("pdftotext")
    if pdf_to_text:
        proc = subprocess.run(
            [pdf_to_text, "-layout", str(path), "-"],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False,
        )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd = [tesseract_bin, str(path), "stdout"]
    if lang_arg:
        cmd.extend(["-l", lang_arg])
    proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
    if proc.returncode != 0 or not proc.stdout.strip():
        raise PreprocessError(f"Image OCR failed: {proc.stderr.strip() or 'no text returned'}")
    return proc.stdout
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def detect_tesseract_langs(tesseract_bin: str) -> Sequence[str]:
    proc = subprocess.run(
        [tesseract_bin, "--list-langs"],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False,
    )
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The skill transmits patient follow-up details, including disease type, visit dates, and notes, to an external LLM endpoint. In a medical context this is sensitive health information, and the manifest/code shown does not provide clear disclosure, consent handling, minimization, or contractual/privacy controls for third-party processing.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The system prompt instructs the model entirely in Chinese and requires markdown output in a specific Chinese-language tone, with no mechanism for user language selection. This is a natural-language locale constraint that appears mandatory rather than optional or region-justified.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
Accepting the API credential via a command-line argument can expose the secret through shell history, process listings, job logs, and orchestration metadata. In shared systems this can allow other users or services to recover the token and make unauthorized requests to the medical LLM backend.

Intent-Code Divergence

Medium
Confidence
88% confidence
Finding
The comment says this block falls back to shared document preprocessing, implying equivalent behavior after preprocessing succeeds. However, line L243 calls `build(data, args.appkey)` even though `build` requires `(data, today, appkey)`, so the fallback path does not actually perform the documented reminder generation behavior and will instead error or misbehave.

Static analysis

Detected: suspicious.dynamic_code_execution

Dynamic code execution detected.

Critical
Code
suspicious.dynamic_code_execution
Location
scripts/run.py:232