From e2870c3344c58086dbae6b8944789c139b7a371c Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Tue, 31 Mar 2026 22:40:35 +0200 Subject: [PATCH] fixing issues --- bincio/edit/server.py | 33 +++++++++++++++++------ site/package.json | 2 ++ site/src/components/ActivityDetail.svelte | 5 ++-- site/src/components/EditDrawer.svelte | 11 ++++---- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/bincio/edit/server.py b/bincio/edit/server.py index b93c397..71fbef1 100644 --- a/bincio/edit/server.py +++ b/bincio/edit/server.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import re import shutil from pathlib import Path from typing import Any @@ -17,14 +18,23 @@ site_url: str = "http://localhost:4321" app = FastAPI(title="BincioActivity Edit Server", docs_url=None, redoc_url=None) -# Allow the Astro dev server (and any local origin) to call the write API +# Allow localhost origins only — this server is never meant to be public app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], - allow_headers=["*"], + allow_origin_regex=r"https?://localhost(:\d+)?", + allow_methods=["GET", "POST", "DELETE"], + allow_headers=["Content-Type"], ) +_VALID_ACTIVITY_ID = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9\-]{0,250}$') + + +def _check_id(activity_id: str) -> str: + """Reject activity IDs that contain path traversal sequences.""" + if not _VALID_ACTIVITY_ID.match(activity_id): + raise HTTPException(400, "Invalid activity ID") + return activity_id + SPORTS = ["cycling", "running", "hiking", "walking", "swimming", "skiing", "other"] STAT_PANELS = ["elevation", "speed", "heart_rate", "cadence", "power"] @@ -348,6 +358,7 @@ async def edit_page(activity_id: str) -> str: @app.get("/api/activity/{activity_id}") async def get_activity(activity_id: str) -> JSONResponse: dd = _get_data_dir() + _check_id(activity_id) json_path = dd / "activities" / f"{activity_id}.json" if not json_path.exists(): raise HTTPException(404, f"Activity {activity_id!r} not found") @@ -383,6 +394,7 @@ async def get_activity(activity_id: str) -> JSONResponse: @app.post("/api/activity/{activity_id}") async def save_activity(activity_id: str, payload: dict[str, Any]) -> JSONResponse: dd = _get_data_dir() + _check_id(activity_id) if not (dd / "activities" / f"{activity_id}.json").exists(): raise HTTPException(404, f"Activity {activity_id!r} not found") @@ -401,9 +413,9 @@ async def save_activity(activity_id: str, payload: dict[str, Any]) -> JSONRespon lines.append("highlight: true") if payload.get("private"): lines.append("private: true") - hide = payload.get("hide_stats") or [] + hide = [s for s in (payload.get("hide_stats") or []) if s in STAT_PANELS] if hide: - lines.append(f"hide_stats: [{', '.join(str(s) for s in hide)}]") + lines.append(f"hide_stats: [{', '.join(hide)}]") description = (payload.get("description") or "").strip() @@ -423,6 +435,7 @@ async def save_activity(activity_id: str, payload: dict[str, Any]) -> JSONRespon @app.post("/api/activity/{activity_id}/images") async def upload_image(activity_id: str, file: UploadFile = File(...)) -> JSONResponse: dd = _get_data_dir() + _check_id(activity_id) if not (dd / "activities" / f"{activity_id}.json").exists(): raise HTTPException(404, f"Activity {activity_id!r} not found") if not file.filename: @@ -532,7 +545,7 @@ async def upload_activity(file: UploadFile = File(...)) -> JSONResponse: """Accept a FIT/GPX/TCX file, extract it, update index.json, and re-merge.""" dd = _get_data_dir() - name = file.filename or "upload.fit" + name = Path(file.filename or "upload.fit").name # strip any path components suffix = _file_suffix(name) if suffix not in _SUPPORTED_SUFFIXES: raise HTTPException(400, f"Unsupported file type '{Path(name).suffix}'. Expected FIT, GPX, or TCX.") @@ -585,7 +598,11 @@ async def upload_activity(file: UploadFile = File(...)) -> JSONResponse: @app.delete("/api/activity/{activity_id}/images/{filename}") async def delete_image(activity_id: str, filename: str) -> JSONResponse: dd = _get_data_dir() - target = dd / "edits" / "images" / activity_id / filename + _check_id(activity_id) + safe_name = Path(filename).name # strip any path traversal + if not safe_name: + raise HTTPException(400, "Invalid filename") + target = dd / "edits" / "images" / activity_id / safe_name if target.exists() and target.is_file(): target.unlink() # Remove empty parent dir diff --git a/site/package.json b/site/package.json index 6aa64c2..6f7466f 100644 --- a/site/package.json +++ b/site/package.json @@ -13,7 +13,9 @@ "@astrojs/svelte": "^7.0.0", "@astrojs/tailwind": "^5.1.0", "@observablehq/plot": "^0.6.0", + "@types/dompurify": "^3.0.5", "astro": "^5.0.0", + "dompurify": "^3.3.3", "maplibre-gl": "^5.0.0", "marked": "^17.0.5", "svelte": "^5.0.0", diff --git a/site/src/components/ActivityDetail.svelte b/site/src/components/ActivityDetail.svelte index 0d012b0..e5f7014 100644 --- a/site/src/components/ActivityDetail.svelte +++ b/site/src/components/ActivityDetail.svelte @@ -1,6 +1,7 @@