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:
Yusuf Suleman
2026-03-29 08:53:04 -05:00
parent d700ba7569
commit fb79f15f75

View File

@@ -1977,7 +1977,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
self.send_response(status)
self.send_header('Content-Type', 'application/json')
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-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
self.end_headers()
@@ -2002,7 +2002,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
def do_OPTIONS(self):
"""Handle CORS preflight."""
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-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
self.end_headers()
@@ -2116,21 +2116,21 @@ class CalorieHandler(BaseHTTPRequestHandler):
# GET /api/entries?date=...&user_id=...
if path == '/api/entries':
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)
return self._send_json(entries)
# GET /api/entries/totals?date=...
if path == '/api/entries/totals':
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)
return self._send_json(totals)
# GET /api/goals/for-date?date=...&user_id=...
if path == '/api/goals/for-date':
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)
if goal:
return self._send_json(goal)
@@ -2138,7 +2138,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
# GET /api/goals?user_id=...
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()
rows = conn.execute(
"SELECT * FROM goals WHERE user_id = ? ORDER BY start_date DESC",
@@ -2190,12 +2190,9 @@ class CalorieHandler(BaseHTTPRequestHandler):
conn.close()
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':
conn = get_db()
rows = conn.execute("SELECT id, username, display_name FROM users").fetchall()
conn.close()
return self._send_json([dict(r) for r in rows])
return self._send_json([{"id": user["id"], "username": user["username"], "display_name": user["display_name"]}])
self._send_json({'error': 'Not found'}, 404)
@@ -2222,7 +2219,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
self.send_response(200)
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('Access-Control-Allow-Origin', '*')
# CORS removed — service is internal only
body = json.dumps({'token': token, 'user': {
'id': user['id'], 'username': user['username'], 'display_name': user['display_name']
}}).encode()
@@ -2583,7 +2580,7 @@ class CalorieHandler(BaseHTTPRequestHandler):
# PUT /api/goals
if path == '/api/goals':
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())
conn = get_db()