T09 · Insecure Skill Coding Practices
- Location
scripts/ingest_workouts.py:125- Finding
DuckDB SQL Injection Through Unescaped Workout Identifiers
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
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.
scripts/ingest_workouts.py:125DuckDB SQL Injection Through Unescaped Workout Identifiers
scripts/report_workouts.py:151Stored Cross-Site Scripting in Generated Health Dashboards
scripts/report_workouts.py:183Remote Leaflet Code Is Loaded Without Subresource Integrity
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.
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.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
def run_duckdb(sql):
result = subprocess.run(
["duckdb", str(DB_PATH), "-c", sql],
capture_output=True, text=True,
)
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
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 calls execute external commands. Without careful input validation, this enables command injection.
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 calls execute external commands. Without careful input validation, this enables command injection.
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 [])]
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.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
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,
)
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.
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.
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
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.
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.
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.
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.