Back to skill

Security audit

CPAP PrismaAPP Log

Security checks for vulnerabilities and agentic risk

Overview

This CPAP logging skill is purpose-aligned, but it needs review because it stores a medical account password in plaintext and can send credentials to a configurable API origin.

Install only if you are comfortable storing PrismaAPP credentials locally and writing sensitive sleep therapy records into your Obsidian vault. Before use, remove or pin api_base to https://my.prismacloud.com, protect config.json carefully, add a real .gitignore entry for it, and avoid enabling cron until you have reviewed where notes will be written.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch-cpap.py:53
Finding
Configurable API Origin Can Receive PrismaAPP Credentials and Bearer Tokens<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-cpap.py:53-69`, `scripts/fetch-cpap.py:229`, and `scripts/fetch-cpap.py:239` **Vulnerability Type**: Unvalidated authentication destination **Risk Level**: High ### Vulnerable Code ```python def login(email: str, password: str, api_base: str) -> str: resp = http_post_form(f"{api_base}/connect/token", { "grant_type": "password", "username": email, "password": password, "scope": "profile offline_access", "tenant": "patientapp", "client_id": "patient-app-client", }) return resp["access_token"] # ── API ──────────────────────────────────────────────────────────────────────── def get_dashboard(token: str, api_base: str) -> dict: return http_get(f"{api_base}/api/Dashboard", token) def get_week(token: str, date_in_week: str, api_base: str) -> list[dict]: data = http_get(f"{api_base}/api/Dashboard/week?dateInWeek={date_in_week}", token) ``` The destination is read directly from configuration and then used for authentication: ```python api_base = cfg.get("api_base", "https://my.prismacloud.com").rstrip("/") ``` ```python token = login(cfg["email"], cfg["password"], api_base) ``` Authenticated requests also transmit the resulting token: ```python def http_get(url: str, token: str) -> dict: req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}", "Accept": "application/json"}) with urllib.request.urlopen(req, timeout=15) as r: return json.loads(r.read()) ``` ### Technical Analysis The Skill's declared functionality requires network access to the PrismaAPP service. Sending credentials to the official authentication endpoint and using a bearer token to retrieve CPAP records are therefore functionally necessary. However, the implementation does not enforce that `api_base` is the documented official origin, `https://my.prismacloud.com`. It accepts any configured ...[truncated 2237 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `api_base` from user-controlled configuration if alternate deployments are not required. 2. Pin all authentication and API requests to `https://my.prismacloud.com`. 3. If configurability is necessary, parse the URL and require: - Scheme exactly equal to `https` - Hostname exactly equal to an explicit allowlisted hostname - Port absent or equal to `443` - No embedded username or password - No fragments or unexpected path prefix 4. Reject redirects to a different origin. Authentication credentials and bearer tokens must never be forwarded across origins. 5. Keep authentication and API endpoint construction centralized rather than accepting arbitrary complete URLs. 6. Fail closed with a clear error when origin validation fails. 7. Add tests covering malicious values such as `http://example.test`, lookalike domains, embedded user information, nonstandard ports, and cross-origin redirects. 8. Prefer a modern authorization flow using revocable, narrowly scoped tokens if the service supports one, rather than repeatedly storing and transmitting the account password. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch-cpap.py:224
Finding
Reusable PrismaAPP Password Is Stored in an Unprotected Plaintext Configuration File<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-cpap.py:224-239`, `config.example.json:1-9`, `README.md:17-33`, and `SKILL.md:17-21` **Vulnerability Type**: Plaintext credential storage **Risk Level**: Medium ### Vulnerable Code The executable reads the reusable password directly from a project-local JSON file: ```python def main(): with open(CONFIG_FILE, encoding="utf-8") as f: cfg = json.load(f) tz = ZoneInfo(cfg.get("timezone", "Europe/Berlin")) t = load_locale(cfg.get("language", "en")) api_base = cfg.get("api_base", "https://my.prismacloud.com").rstrip("/") log_dir = Path(cfg["vault_path"]) / cfg.get("log_dir", "30 Bereiche/Gesundheit/CPAP/Logs") log_dir.mkdir(parents=True, exist_ok=True) args = sys.argv[1:] do_all = "--all" in args from_arg = next((a for a in args if a.startswith("--from=")), None) date_arg = next((a for a in args if not a.startswith("--")), None) print("Logging in ...") token = login(cfg["email"], cfg["password"], api_base) ``` The supplied example explicitly reserves a plaintext password field: ```json { "email": "your@email.com", "password": "your_password", "api_base": "https://my.prismacloud.com", "timezone": "Europe/Berlin", "vault_path": "/path/to/your/obsidian/vault", "log_dir": "30 Bereiche/Gesundheit/CPAP/Logs", "language": "en" } ``` The README directs the user to copy this file and enter credentials: ```bash cp config.example.json config.json # Edit config.json ``` It also states that `config.json` is listed in `.gitignore`, but no `.gitignore` file was present in the audited project. ### Technical Analysis The Skill requires users to keep their reusable PrismaAPP account password in `config.json`. The file is opened as ordinary plaintext, and the implementation does not verify or restrict its filesystem permissions. Plaintext storage makes the password available to any local process or account that can ...[truncated 1901 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Store credentials in an operating-system keychain or established secret manager rather than `config.json`. 2. Where supported, use a revocable, narrowly scoped access or refresh token instead of the user's reusable account password. 3. If file-based storage must remain available: - Store secrets in a separate file outside the project tree. - Create that file with permission mode `0600`. - Refuse to run, or prominently warn, when group or other users can read it. - Avoid placing secrets inside synchronized vaults or repositories. 4. Add an actual root `.gitignore` containing at least: ```gitignore /config.json ``` 5. Add a startup check that warns if `config.json` is tracked by Git. 6. Keep `config.example.json` limited to placeholders and clearly document that it must never contain real credentials. 7. Document credential rotation steps and advise immediate rotation if the configuration is committed, shared, or backed up insecurely. 8. Avoid command-line password arguments because they can be exposed through shell history and process listings. ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (12)

Credential Access

High
Category
Privilege Escalation
Content
| Endpoint | Description |
|----------|-------------|
| `POST /connect/token` | OAuth2 password grant → access token |
| `GET /api/Dashboard` | Returns `minDate` (first sync) and `serialNumber` |
| `GET /api/Dashboard/week?dateInWeek=YYYY-MM-DD` | Week data (7 days) |
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill performs network access to a third-party medical API and writes files into a local Obsidian vault, but the manifest declares no explicit tool scope such as permissions or allowed-tools. That creates an overbroad trust boundary: an agent may execute the skill with capabilities the user cannot readily inspect or constrain, increasing the risk of unintended file modification or unauthorized outbound requests.

Session Persistence

Medium
Category
Rogue Agent
Content
## When Triggered

- User says: "CPAP log", "CPAP data", "write sleep therapy", "prisma log", "CPAP today/yesterday/[date]"
- Daily cron job fires (configurable)
- User requests a historical backfill
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
This file defines all user-facing strings exclusively in Spanish, which indicates the skill can present output in a fixed language. Under the policy rule, forcing a specific language without user opt-in or a clearly documented locale-specific justification is a natural-language policy concern.

Natural-Language Policy Violations

Medium
Confidence
84% confidence
Finding
This localization file defines all user-facing strings exclusively in French, which can contribute to a language/locale policy violation if the skill forces this locale without user opt-in. There is no natural-language indication in the file that French is optional, selectable, or restricted to a justified region-specific context.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This code logs into a medical-data service, retrieves therapy information, and persists it to the configured vault path. Although it prints progress messages, those messages do not warn that sensitive health information will be downloaded and stored locally, which is a safety-relevant disclosure gap for a code file handling personal health data.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
In backfill mode, the script loads all available days and writes one markdown file per day into the user's vault. The status output reports progress, but it does not disclose up front that a potentially large amount of sensitive historical medical data will be imported into local storage.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
This localization file contains only German strings, which can reflect a language-specific behavior if used as the sole or forced output locale. Under the policy, forcing a specific language without user opt-in can be a natural-language policy violation unless the locale constraint is clearly documented and justified.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
This localization file defines all user-visible strings in Italian, which suggests the skill may present content in a fixed language. Under the policy, forcing a specific language without user opt-in can be a natural-language policy violation unless the locale restriction is explicitly documented and justified.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
This localization file contains only Polish-language strings, which can represent a language-policy concern if the skill forces this locale without user selection. The file itself does not show any opt-in, fallback, or documentation that Polish is optional or region-specific.

Natural-Language Policy Violations

Low
Confidence
81% confidence
Finding
This localization file contains only Portuguese user-facing strings, and there is no natural-language indication in the file that the language is optional, selectable, or limited to a justified region-specific context. Under the policy, forcing a specific language without user opt-in can be a violation.

Natural-Language Policy Violations

Low
Confidence
86% confidence
Finding
This locale file contains only Serbian-language strings for user-facing output, with no indication in the text that language selection is optional or user-driven. Per the policy, forcing a specific language without user opt-in can be a natural-language locale policy violation.

Static analysis

No suspicious patterns detected.