fix(fitness): eliminate cross-user data access (#4)
- All user_id query params now enforced to authenticated user's own ID - /api/users restricted to return only current user (no user enumeration) - Wildcard CORS headers removed (service is internal-only via gateway) - Covers: entries, totals, goals, templates, favorites, goal setting Closes #4
This commit is contained in:
@@ -1977,7 +1977,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_response(status)
|
self.send_response(status)
|
||||||
self.send_header('Content-Type', 'application/json')
|
self.send_header('Content-Type', 'application/json')
|
||||||
self.send_header('Content-Length', len(body))
|
self.send_header('Content-Length', len(body))
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
# CORS removed — service is internal only
|
||||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-Telegram-User-Id')
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-Telegram-User-Id')
|
||||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -2002,7 +2002,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
|
|||||||
def do_OPTIONS(self):
|
def do_OPTIONS(self):
|
||||||
"""Handle CORS preflight."""
|
"""Handle CORS preflight."""
|
||||||
self.send_response(204)
|
self.send_response(204)
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
# CORS removed — service is internal only
|
||||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-Telegram-User-Id')
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-Telegram-User-Id')
|
||||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -2116,21 +2116,21 @@ class CalorieHandler(BaseHTTPRequestHandler):
|
|||||||
# GET /api/entries?date=...&user_id=...
|
# GET /api/entries?date=...&user_id=...
|
||||||
if path == '/api/entries':
|
if path == '/api/entries':
|
||||||
entry_date = params.get('date', [date.today().isoformat()])[0]
|
entry_date = params.get('date', [date.today().isoformat()])[0]
|
||||||
target_user = params.get('user_id', [user['id']])[0]
|
target_user = user['id'] # enforced: no cross-user access
|
||||||
entries = get_entries_by_date(target_user, entry_date)
|
entries = get_entries_by_date(target_user, entry_date)
|
||||||
return self._send_json(entries)
|
return self._send_json(entries)
|
||||||
|
|
||||||
# GET /api/entries/totals?date=...
|
# GET /api/entries/totals?date=...
|
||||||
if path == '/api/entries/totals':
|
if path == '/api/entries/totals':
|
||||||
entry_date = params.get('date', [date.today().isoformat()])[0]
|
entry_date = params.get('date', [date.today().isoformat()])[0]
|
||||||
target_user = params.get('user_id', [user['id']])[0]
|
target_user = user['id'] # enforced: no cross-user access
|
||||||
totals = get_daily_totals(target_user, entry_date)
|
totals = get_daily_totals(target_user, entry_date)
|
||||||
return self._send_json(totals)
|
return self._send_json(totals)
|
||||||
|
|
||||||
# GET /api/goals/for-date?date=...&user_id=...
|
# GET /api/goals/for-date?date=...&user_id=...
|
||||||
if path == '/api/goals/for-date':
|
if path == '/api/goals/for-date':
|
||||||
for_date = params.get('date', [date.today().isoformat()])[0]
|
for_date = params.get('date', [date.today().isoformat()])[0]
|
||||||
target_user = params.get('user_id', [user['id']])[0]
|
target_user = user['id'] # enforced: no cross-user access
|
||||||
goal = get_goals_for_date(target_user, for_date)
|
goal = get_goals_for_date(target_user, for_date)
|
||||||
if goal:
|
if goal:
|
||||||
return self._send_json(goal)
|
return self._send_json(goal)
|
||||||
@@ -2138,7 +2138,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
# GET /api/goals?user_id=...
|
# GET /api/goals?user_id=...
|
||||||
if path == '/api/goals':
|
if path == '/api/goals':
|
||||||
target_user = params.get('user_id', [user['id']])[0]
|
target_user = user['id'] # enforced: no cross-user access
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT * FROM goals WHERE user_id = ? ORDER BY start_date DESC",
|
"SELECT * FROM goals WHERE user_id = ? ORDER BY start_date DESC",
|
||||||
@@ -2190,12 +2190,9 @@ class CalorieHandler(BaseHTTPRequestHandler):
|
|||||||
conn.close()
|
conn.close()
|
||||||
return self._send_json([dict(r) for r in rows])
|
return self._send_json([dict(r) for r in rows])
|
||||||
|
|
||||||
# GET /api/users (list all users — for switching view)
|
# GET /api/users — restricted: only returns the current user
|
||||||
if path == '/api/users':
|
if path == '/api/users':
|
||||||
conn = get_db()
|
return self._send_json([{"id": user["id"], "username": user["username"], "display_name": user["display_name"]}])
|
||||||
rows = conn.execute("SELECT id, username, display_name FROM users").fetchall()
|
|
||||||
conn.close()
|
|
||||||
return self._send_json([dict(r) for r in rows])
|
|
||||||
|
|
||||||
self._send_json({'error': 'Not found'}, 404)
|
self._send_json({'error': 'Not found'}, 404)
|
||||||
|
|
||||||
@@ -2222,7 +2219,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header('Content-Type', 'application/json')
|
self.send_header('Content-Type', 'application/json')
|
||||||
self.send_header('Set-Cookie', f'session={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000')
|
self.send_header('Set-Cookie', f'session={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000')
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
# CORS removed — service is internal only
|
||||||
body = json.dumps({'token': token, 'user': {
|
body = json.dumps({'token': token, 'user': {
|
||||||
'id': user['id'], 'username': user['username'], 'display_name': user['display_name']
|
'id': user['id'], 'username': user['username'], 'display_name': user['display_name']
|
||||||
}}).encode()
|
}}).encode()
|
||||||
@@ -2583,7 +2580,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
|
|||||||
# PUT /api/goals
|
# PUT /api/goals
|
||||||
if path == '/api/goals':
|
if path == '/api/goals':
|
||||||
data = self._read_body()
|
data = self._read_body()
|
||||||
target_user = data.get('user_id', user['id'])
|
target_user = user['id'] # enforced: no cross-user access
|
||||||
start_date = data.get('start_date', date.today().isoformat())
|
start_date = data.get('start_date', date.today().isoformat())
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
|
|||||||
Reference in New Issue
Block a user