fix(trips): enforce password protection on shared trips (#3)

- handle_share_api now checks X-Share-Password header against bcrypt hash
  before returning trip data. Returns 401 with {protected: true} if password
  required but not provided/incorrect
- share_password now stored as bcrypt hash, not plaintext
- All plaintext password logging removed from handle_share_verify
- handle_share_verify uses bcrypt.checkpw instead of string equality
- Migration invalidates existing plaintext share passwords (< 50 chars)
- Removed dead hash_password function (used hashlib.sha256)
- Added bcrypt to trips Dockerfile

Closes #3
This commit is contained in:
Yusuf Suleman
2026-03-29 08:50:45 -05:00
parent 6bd23e7e8b
commit d700ba7569
3 changed files with 91 additions and 20 deletions

View File

@@ -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 users 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.

View File

@@ -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 . .

View File

@@ -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,))