Back to skill

Security audit

Health Metrics

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly purpose-aligned, but it handles sensitive health and GPS data with verified SQL and HTML injection risks that users should review before installing.

Install only if you trust the Apple Health export files being ingested and are comfortable keeping a persistent local database plus generated reports containing sensitive health and location data. Store outputs in a private directory, avoid opening generated workout reports from modified or untrusted exports, and prefer a version that escapes SQL/HTML values and bundles or integrity-protects map assets.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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

Error
Location
scripts/ingest_workouts.py:125
Finding

DuckDB SQL Injection Through Unescaped Workout Identifiers

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/report_workouts.py:151
Finding

Stored Cross-Site Scripting in Generated Health Dashboards

Content
View full analysis
{value}
{label}
') ``` The workout index directly interpolates the workout name: ```python for w in workouts: link = f'{w["date"]}-{slugify(w["name"])}.html' cal = fmt(w["active_energy_kj"] / 4.184, "", 0) if w["active_energy_kj"] is not None else "–" dist = fmt(w["distance_km"], " km") if w["distance_km"] is not None else "–" html.append(f'{w["date"]}{w["name"]}' f'{fmt_duration(w["duration_s"])}{dist}{cal}') ``` The training-load report exposes the same workout-name sink: ```python for w, load in list(reversed(loads))[:20]: dur_min = (w["duration_s"] or 0) / 60 avg_hr_str = f'{w["avg_hr"]:.0f}' if w["avg_hr"] else "–" html.append(f'{w["date"]}{w["name"]}{dur_min:.0f} min' f'{avg_hr_str}{load:.0f}') ``` ### Technical Analysis Workout `name` and `location` fields originate in imported JSON and are stored in DuckDB. Report generat ...[truncated 2004 chars]
Remediation
View remediation
` termination - URLs and filenames: dedicated URL/path validation rather than HTML escaping 4. Escape workout names in `HTML_HEAD`, detail-page headings, index rows, and training-load table rows. 5. Escape location values before building summary tiles. 6. Add a restrictive Content Security Policy. A locally bundled script with nonce- or hash-based authorization is preferable to allowing arbitrary inline execution. 7. Add regression tests with values containing ``. 8. Regenerate existing reports after deploying the fix because already generated HTML files remain vulnerable. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/report_workouts.py:183
Finding

Remote Leaflet Code Is Loaded Without Subresource Integrity

Content
View full analysis
Workout — {title} ``` It also loads remote JavaScript and map tiles when a route is present: ```python html.append('<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>') html.append('<script>') html.append(f'const route = {json.dumps(simplified)};') html.append(""" const map = L.map('map'); L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Tiles &copy; Esri', maxZoom: 19 }).addTo(map); ``` ### Technical Analysis Generated workout reports execute Leaflet JavaScript directly from `unpkg.com` each time an online user opens a route report. Although the URL pins Leaflet to version `1.9.4`, the script lacks a Subresource Integrity hash and is not bundled with the audited project. Version pinning prevents normal version drift but does not protect against compromise of the CDN, package artifact, DNS/TLS trust chain, or upstream distribution account. If the bytes returned by the CDN change maliciously, the browser will execute them in the same document that contains personal health data and route coordinates. The Esri tile requests are expected map functionality and are disclosed by the documentation. Nevertheless, requesting the tiles reveals the geographic tile area being viewed, together with normal client network metadata, to the tile provider. ### Attack Path 1. A victim generates a workout report containing a GPS route. 2. The victim opens the report while connected to the internet. 3. The browser requests `leaflet.js` f ...[truncated 1084 chars]
Remediation
View remediation
``` The actual hashes must be independently verified against the intended release rather than copied from an untrusted response. 3. Add a restrictive Content Security Policy limiting scripts to the bundled resource or exact integrity-protected source and limiting network connections to required tile hosts. 4. Offer an offline map-free mode that does not contact Unpkg or Esri. 5. Clearly warn users that opening route maps contacts both the CDN and tile provider and that tile requests may reveal approximate route geography. 6. Pin and periodically verify the exact Leaflet artifact checksum as part of release review. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (14)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding

The skill explicitly instructs the agent to execute shell commands, consume environment variables, and write files, but it does not declare any corresponding tool scope or allowed-tools policy. That creates a least-privilege and reviewability gap: an agent may invoke broader shell/file capabilities than a user or platform expects, increasing the chance of unintended file access, writes, or command execution against sensitive local health-data paths.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

This skill processes highly sensitive Apple Health data from a default export directory and stores it in a local DuckDB database without any explicit consent prompt, warning, or guidance on data sensitivity and storage location. In the context of a health-data ingestion skill, silent handling of medical, sleep, and workout records increases privacy risk through accidental collection, retention, or exposure on a shared machine.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/ingest_health_metrics.py (reported line 59)May include surrounding context.

python
def run_duckdb(sql):
    result = subprocess.run(
        ["duckdb", str(DB_PATH), "-c", sql],
        capture_output=True, text=True,
    )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/ingest_workouts.py (reported line 60)May include surrounding context.

python
def run_duckdb(sql):
    result = subprocess.run(["duckdb", str(DB_PATH), "-c", sql], capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(f"duckdb failed:\n{sql}\n---\n{result.stderr}")
    return result.stdout

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/ingest_workouts.py (reported line 108)May include surrounding context.

python
treat a JSON-typed field as effectively absent."""
    sql = (f"SELECT w.* FROM (SELECT unnest(data.workouts) AS w "
           f"FROM read_json_auto('{file_literal}', sample_size=-1, maximum_object_size=500000000)) t LIMIT 0;")
    result = subprocess.run(["duckdb", str(DB_PATH), "-json", "-c", f"DESCRIBE {sql}"],
                             capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(result.stderr)

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/ingest_workouts.py (reported line 132)May include surrounding context.

python
WITH raw AS (SELECT unnest(data.workouts) AS w FROM read_json_auto('{file_literal}', sample_size=-1, maximum_object_size=500000000))
    SELECT w.id AS id FROM raw;
    """
    result = subprocess.run(["duckdb", str(DB_PATH), "-json", "-c", ids_sql], capture_output=True, text=True)
    if result.returncode != 0:
        raise RuntimeError(result.stderr)
    ids = [r["id"] for r in (json.loads(result.stdout.strip()) if result.stdout.strip() else [])]

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The chart() function builds HTML and inline JavaScript with f-strings that directly interpolate chart_id and y_fmt_js, while labels/series names are later rendered into SVG/text and tooltip HTML via innerHTML in the browser. If any of those values come from imported Apple Health export data or any other untrusted source, an attacker can inject script-breaking payloads or HTML/JS that executes when the offline dashboard is opened, resulting in stored XSS in a local file context.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/lib/query.py (reported line 21)May include surrounding context.

python
def query(sql):
    # Resolve at call time (not import time) so a late --db / env change still applies.
    result = subprocess.run(
        ["duckdb", str(db_path()), "-json", "-c", sql],
        capture_output=True, text=True,
    )

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The generated workout pages load Leaflet from a public CDN and satellite tiles from Esri at view time, which causes user workout report access to make outbound network requests to third parties. In this health-data context, that can leak sensitive metadata such as the viewer's IP address, access timing, and potentially route/location interest, undermining the expectation of offline/local processing for private health information.

Content

No source excerpt is available for this finding.

Scope Creep

Low
Category
Excessive Agency
Confidence
70% confidence
Finding

Skill's behavior or capabilities extend beyond its stated purpose. Scope creep allows an agent to perform actions unrelated to its documented functionality, increasing the attack surface.

Content

Scanner excerpt · LICENSE (reported line 12)May include surrounding context.

text
permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
85% confidence
Finding

This markdown file explains that report scripts write full HTML/Markdown outputs to an output directory, including a per-day AI/LLM-oriented summary, but it does not include any explicit warning that these generated files contain sensitive personal health data. Because SQP-2 applies to markdown files when behavior could affect user data or privacy, the omission is a relevant disclosure gap.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Low
Category
Not specified by scanner
Confidence
92% confidence
Finding

The manifest describes rendering offline HTML dashboards and Markdown summaries from local Apple Health exports. However, this document states that workout detail pages use Leaflet with Esri World Imagery tiles from public CDNs when viewed, so that portion is not fully offline as the description suggests.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
81% confidence
Finding

This code performs file writes for per-workout HTML reports, and with the documented --force option the script will overwrite previously generated pages. Although the script's purpose is report generation, there is no explicit warning at the write site or in a docstring/comment that files on disk will be modified, which is the kind of disclosure this rule asks to surface for file-writing operations.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
80% confidence
Finding

The script writes index.html directly to the output directory, replacing any prior generated index file. While this aligns with the script's report-building purpose, there is no explicit disclosure in comments, docstrings, or user-visible messaging before the write that filesystem contents will be changed.

Content

No source excerpt is available for this finding.