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:
@@ -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 . .
|
||||
|
||||
|
||||
@@ -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,))
|
||||
|
||||
Reference in New Issue
Block a user