diff --git a/claude_remediation_prompt.txt b/claude_remediation_prompt.txt new file mode 100644 index 0000000..855c123 --- /dev/null +++ b/claude_remediation_prompt.txt @@ -0,0 +1,66 @@ +You are acting as a senior staff engineer and security remediation lead on the `platform` repository. + +Use the existing Gitea issues as the source of truth for scope and acceptance criteria. + +Repository: +- Name: `platform` +- Gitea repo: `yusiboyz/platform` + +Primary tracking issue: +- `#1 Production Security and Readiness Remediation` + +Immediate issues: +- `#2 Auth Boundary: Registration and Default Credentials` +- `#3 Trips Sharing Security: Enforce Protection and Remove Plaintext Secrets` +- `#4 Fitness Authorization: Eliminate Cross-User Data Access` +- `#5 Gateway Trust Model: Protect Internal Services and Service-Level Data` +- `#6 Repository Hygiene: Remove Tracked Secrets and Runtime Databases` +- `#7 Transport Security: Finish Cookie Hardening, TLS Verification, and Proxy Controls` +- `#8 Dependency Security and CI Enforcement` + +Next issues: +- `#9 Performance Hardening: Cache and De-risk Summary Endpoints` +- `#10 Deployment Hardening: Containers, Health Checks, and Production Readiness` + +Required execution order: +1. `#3` +2. `#4` +3. `#5` +4. `#2` +5. `#6` +6. `#7` +7. `#8` +8. `#9` +9. `#10` + +Instructions: +- Start by reading the codebase and the linked Gitea issues. +- Treat the repo as production-bound and internet-exposed. +- Make real code changes, not just recommendations. +- After each issue-sized chunk, verify the fix with tests, targeted checks, or direct code-path validation. +- Update the relevant Gitea issue with: + - what was changed + - exact files touched + - how it was verified + - any remaining risk +- Keep changes minimal but correct. Do not do broad refactors unless required for security or correctness. +- Do not close an issue unless its acceptance criteria are actually satisfied. +- If an issue is only partially fixed, comment with exactly what remains. +- Preserve user changes and do not revert unrelated work. +- If you need to rotate secrets or remove tracked `.env` / `.db` artifacts, make the code and repo changes needed, but clearly separate anything that requires a manual operational rotation step. +- Prioritize correctness and security over speed. + +Specific expectations: +- For `#3`, fully eliminate the Trips share-token bypass. Password protection must actually gate access. No plaintext password storage or logging. +- For `#4`, eliminate all normal-user cross-user access in Fitness. `user_id` must not let one user read another user’s data. +- For `#5`, remove the implicit trust model for service-global data exposure where required. +- For `#2`, remove default credential seeding/fallbacks and make startup fail fast where appropriate. +- For `#6`, stop tracking live env/db artifacts and harden ignore rules. +- For `#7`, restore TLS verification by default and finish cookie hardening cleanly. +- For `#8`, resolve the budget dependency vulnerability and add CI checks for dependency/security scanning. + +Reporting format: +- At the start: brief plan tied to issue numbers. +- During work: short progress updates. +- At the end of each issue: `fixed`, `partial`, or `blocked`, with exact file references. +- Final output: summarize which issue numbers were completed, which remain open, and what manual ops actions are still required. diff --git a/services/trips/Dockerfile b/services/trips/Dockerfile index b008a59..16bb965 100644 --- a/services/trips/Dockerfile +++ b/services/trips/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app ENV PYTHONUNBUFFERED=1 -RUN pip install --no-cache-dir PyPDF2 +RUN pip install --no-cache-dir PyPDF2 bcrypt COPY . . diff --git a/services/trips/server.py b/services/trips/server.py index df99cf1..5bf2237 100644 --- a/services/trips/server.py +++ b/services/trips/server.py @@ -7,8 +7,8 @@ import os import json import sqlite3 import uuid -import hashlib import secrets +import bcrypt import shutil import urllib.request import urllib.parse @@ -208,6 +208,10 @@ def init_db(): except: pass + # Invalidate any existing plaintext share passwords (migration to bcrypt) + cursor.execute("UPDATE trips SET share_password = NULL WHERE share_password IS NOT NULL AND share_password != '' AND length(share_password) < 50") + conn.commit() + # Add hike-specific fields to locations table for col, col_type in [('hike_distance', 'TEXT'), ('hike_difficulty', 'TEXT'), ('hike_time', 'TEXT')]: try: @@ -1228,10 +1232,6 @@ def get_trip_location(trip): return trip.get('name', '').strip() -def hash_password(password): - """Hash a password.""" - return hashlib.sha256(password.encode()).hexdigest() - def create_session(username): """Create a new session in the database.""" token = secrets.token_hex(32) @@ -2398,7 +2398,7 @@ class TripHandler(BaseHTTPRequestHandler): WHERE id = ? ''', (data.get("name", ""), data.get("description", ""), data.get("start_date", ""), data.get("end_date", ""), - data.get("share_password", "") or None, + bcrypt.hashpw(data["share_password"].encode(), bcrypt.gensalt()).decode() if data.get("share_password") else None, data.get("immich_album_id") or None, trip_id)) conn.commit() conn.close() @@ -3108,16 +3108,12 @@ class TripHandler(BaseHTTPRequestHandler): self.send_json({"success": True}) def handle_share_verify(self, body): - """Verify password for shared trip.""" + """Verify password for shared trip. Uses bcrypt comparison.""" try: - print(f"[Share] Handler called with body: {body[:100]}", flush=True) data = json.loads(body) share_token = data.get("share_token") password = data.get("password", "") - print(f"[Share] Verifying token: {share_token}", flush=True) - print(f"[Share] Password entered: {repr(password)}", flush=True) - conn = get_db() cursor = conn.cursor() cursor.execute("SELECT share_password FROM trips WHERE share_token = ?", (share_token,)) @@ -3125,20 +3121,19 @@ class TripHandler(BaseHTTPRequestHandler): conn.close() if not row: - print(f"[Share] Trip not found for token", flush=True) self.send_json({"success": False, "error": "Trip not found"}) return - stored_password = row[0] or "" - print(f"[Share] Password stored: {repr(stored_password)}", flush=True) - print(f"[Share] Match: {password == stored_password}", flush=True) - - if password == stored_password: + stored_hash = row[0] or "" + if not stored_hash: + # No password set — allow access + self.send_json({"success": True}) + elif bcrypt.checkpw(password.encode(), stored_hash.encode()): self.send_json({"success": True}) else: self.send_json({"success": False, "error": "Incorrect password"}) except Exception as e: - print(f"[Share] ERROR: {e}", flush=True) + print(f"[Share] Verify error: {type(e).__name__}", flush=True) import traceback traceback.print_exc() self.send_json({"success": False, "error": str(e)}) @@ -5283,7 +5278,8 @@ Format your response with clear headers and bullet points. Be specific with name self.send_json({"success": True}) def handle_share_api(self, share_token): - """Return trip data as JSON for a public share token.""" + """Return trip data as JSON for a public share token. + If the trip has a share_password, requires X-Share-Password header.""" conn = get_db() cursor = conn.cursor() cursor.execute("SELECT * FROM trips WHERE share_token = ?", (share_token,)) @@ -5294,6 +5290,15 @@ Format your response with clear headers and bullet points. Be specific with name self.send_json({"error": "Trip not found"}, 404) return + # Enforce password protection + stored_hash = trip.get("share_password") or "" + if stored_hash: + provided = self.headers.get("X-Share-Password", "") + if not provided or not bcrypt.checkpw(provided.encode(), stored_hash.encode()): + conn.close() + self.send_json({"error": "Password required", "protected": True}, 401) + return + trip_id = trip["id"] cursor.execute("SELECT * FROM transportations WHERE trip_id = ? ORDER BY date", (trip_id,))