From 94c0467d12b3167b489cb5608f7e638a9129298c Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Sat, 2 May 2026 21:34:39 -0400 Subject: [PATCH] 05/02/2026 updated code for security 6 --- app/routes/auth.py | 11 +++++++++++ app/routes/emergency.py | 13 ++++++++++--- app/routes/folders.py | 8 ++++++-- app/routes/sharing.py | 10 +++++++++- app/routes/vault.py | 9 ++++++++- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/app/routes/auth.py b/app/routes/auth.py index de6853b..d1ab92b 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -191,6 +191,7 @@ def login(): @auth_bp.route('/logout', methods=['POST']) +@limiter.limit('60 per minute') def logout(): """Blacklist both the access token (from header) and refresh token (from body).""" auth_header = request.headers.get('Authorization', '') @@ -230,6 +231,7 @@ def refresh(): # ── MFA / TOTP endpoints ───────────────────────────────────────────────────── @auth_bp.route('/mfa/setup', methods=['GET']) +@limiter.limit('10 per minute') @require_jwt def mfa_setup(): """Generate a new TOTP secret and return QR code (as base64 PNG data URI).""" @@ -261,6 +263,7 @@ def mfa_setup(): @auth_bp.route('/mfa/enable', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def mfa_enable(): """Enable MFA after verifying the first TOTP code. Returns one-time backup codes.""" @@ -306,6 +309,7 @@ def mfa_enable(): @auth_bp.route('/mfa/disable', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def mfa_disable(): """Disable MFA after verifying the current TOTP code or a backup code.""" @@ -420,6 +424,7 @@ def mfa_verify(): @auth_bp.route('/mfa/status', methods=['GET']) +@limiter.limit('60 per minute') @require_jwt def mfa_status(): user = db.session.get(User, g.current_user_id) @@ -432,6 +437,7 @@ def mfa_status(): @auth_bp.route('/mfa/backup-codes/regenerate', methods=['POST']) +@limiter.limit('5 per minute') @require_jwt def mfa_backup_codes_regenerate(): """ @@ -474,6 +480,7 @@ def mfa_backup_codes_regenerate(): @auth_bp.route('/me', methods=['GET']) +@limiter.limit('60 per minute') @require_jwt def me(): """Return basic profile info for the authenticated user.""" @@ -532,6 +539,7 @@ def audit_log(): # ── Account management ──────────────────────────────────────────────────────── @auth_bp.route('/change-password', methods=['POST']) +@limiter.limit('5 per minute') @require_jwt def change_password(): """ @@ -622,6 +630,7 @@ def change_password(): @auth_bp.route('/account', methods=['DELETE']) +@limiter.limit('3 per minute') @require_jwt def delete_account(): """ @@ -673,6 +682,7 @@ def delete_account(): # ── Account Recovery ────────────────────────────────────────────────────────── @auth_bp.route('/recovery/setup', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def recovery_setup(): """ @@ -710,6 +720,7 @@ def recovery_setup(): @auth_bp.route('/recovery/status', methods=['GET']) +@limiter.limit('60 per minute') @require_jwt def recovery_status(): """Return whether the user has a recovery code configured.""" diff --git a/app/routes/emergency.py b/app/routes/emergency.py index c2cd2be..d19b40f 100644 --- a/app/routes/emergency.py +++ b/app/routes/emergency.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone from flask import Blueprint, request, jsonify, g -from app import db +from app import db, limiter from app.models.user import User from app.models.emergency_access import EmergencyAccess from app.models.audit_log import AuditLog @@ -15,6 +15,7 @@ def _client_ip(): @emergency_bp.route('', methods=['GET']) +@limiter.limit('30 per minute') @require_jwt def list_emergency(): """Return emergency access records both as grantor and as grantee.""" @@ -45,6 +46,7 @@ def _ea_as_grantee(ea: EmergencyAccess) -> dict: @emergency_bp.route('', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def create_emergency(): """Grantor creates an emergency access invitation for a trusted contact.""" @@ -101,6 +103,7 @@ def create_emergency(): @emergency_bp.route('/', methods=['DELETE']) +@limiter.limit('10 per minute') @require_jwt def delete_emergency(ea_id): """Grantor removes an emergency access grant.""" @@ -125,6 +128,7 @@ def delete_emergency(ea_id): @emergency_bp.route('//accept', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def accept_emergency(ea_id): """Grantee accepts an emergency access invitation.""" @@ -157,6 +161,7 @@ def accept_emergency(ea_id): @emergency_bp.route('//provide', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def provide_vault(ea_id): """ @@ -194,6 +199,7 @@ def provide_vault(ea_id): @emergency_bp.route('//request', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def request_access(ea_id): """Grantee initiates an access request, starting the wait-period clock.""" @@ -226,6 +232,7 @@ def request_access(ea_id): @emergency_bp.route('//deny', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def deny_access(ea_id): """Grantor denies a pending access request (resets to ready).""" @@ -253,6 +260,7 @@ def deny_access(ea_id): @emergency_bp.route('//vault', methods=['GET']) +@limiter.limit('10 per minute') @require_jwt def get_emergency_vault(ea_id): """ @@ -293,5 +301,4 @@ def get_emergency_vault(ea_id): return jsonify({ 'enc_vault': ea.enc_vault, 'grantor_public_key': grantor.sharing_public_key if grantor else None, - }), 200 - + }), 200 \ No newline at end of file diff --git a/app/routes/folders.py b/app/routes/folders.py index 2701b7f..3826f50 100644 --- a/app/routes/folders.py +++ b/app/routes/folders.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, jsonify, g -from app import db +from app import db, limiter from app.models.folder import Folder from app.models.audit_log import AuditLog from app.services.auth_service import require_jwt @@ -13,6 +13,7 @@ def _client_ip(): @folders_bp.route('', methods=['GET']) +@limiter.limit('120 per minute') @require_jwt def list_folders(): folders = Folder.query.filter_by(user_id=g.current_user_id).order_by(Folder.name.asc()).all() @@ -20,6 +21,7 @@ def list_folders(): @folders_bp.route('', methods=['POST']) +@limiter.limit('30 per minute') @require_jwt def create_folder(): data = request.get_json(silent=True) or {} @@ -43,6 +45,7 @@ def create_folder(): @folders_bp.route('/', methods=['PUT']) +@limiter.limit('30 per minute') @require_jwt def update_folder(folder_id): folder = Folder.query.filter_by(id=folder_id, user_id=g.current_user_id).first() @@ -68,6 +71,7 @@ def update_folder(folder_id): @folders_bp.route('/', methods=['DELETE']) +@limiter.limit('30 per minute') @require_jwt def delete_folder(folder_id): folder = Folder.query.filter_by(id=folder_id, user_id=g.current_user_id).first() @@ -86,4 +90,4 @@ def delete_folder(folder_id): ip_address=_client_ip(), ) db.session.commit() - return jsonify({'message': 'Folder deleted'}), 200 + return jsonify({'message': 'Folder deleted'}), 200 \ No newline at end of file diff --git a/app/routes/sharing.py b/app/routes/sharing.py index d005d01..00ecd8a 100644 --- a/app/routes/sharing.py +++ b/app/routes/sharing.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, jsonify, g -from app import db +from app import db, limiter from app.models.user import User from app.models.shared_item import SharedItem from app.models.audit_log import AuditLog @@ -15,6 +15,7 @@ def _client_ip(): # ── Sharing keypair management ──────────────────────────────────────────────── @sharing_bp.route('/keys', methods=['GET']) +@limiter.limit('60 per minute') @require_jwt def get_my_keys(): """Return current user's encrypted sharing private key (to decrypt client-side).""" @@ -30,6 +31,7 @@ def get_my_keys(): @sharing_bp.route('/keys', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def store_my_keys(): """Store ECDH keypair. Public key plaintext; private key encrypted with vault key.""" @@ -61,6 +63,7 @@ def store_my_keys(): @sharing_bp.route('/public-key', methods=['GET']) +@limiter.limit('30 per minute') @require_jwt def get_public_key(): """Look up another user's ECDH public key by email (needed to create a share).""" @@ -84,6 +87,7 @@ def get_public_key(): # ── Outgoing shares ─────────────────────────────────────────────────────────── @sharing_bp.route('', methods=['GET']) +@limiter.limit('60 per minute') @require_jwt def list_outgoing(): """List all items the current user has shared with others.""" @@ -103,6 +107,7 @@ def list_outgoing(): @sharing_bp.route('', methods=['POST']) +@limiter.limit('30 per minute') @require_jwt def create_share(): """ @@ -164,6 +169,7 @@ def create_share(): @sharing_bp.route('/', methods=['DELETE']) +@limiter.limit('30 per minute') @require_jwt def delete_share(share_id): share = SharedItem.query.filter_by(id=share_id, owner_id=g.current_user_id).first() @@ -190,6 +196,7 @@ def delete_share(share_id): # ── Inbox (received shares) ─────────────────────────────────────────────────── @sharing_bp.route('/inbox', methods=['GET']) +@limiter.limit('60 per minute') @require_jwt def inbox(): """List all items shared with the current user.""" @@ -216,6 +223,7 @@ def inbox(): @sharing_bp.route('/inbox//accept', methods=['POST']) +@limiter.limit('30 per minute') @require_jwt def accept_share(share_id): """Mark a received share as accepted (links recipient_id if not already set).""" diff --git a/app/routes/vault.py b/app/routes/vault.py index 654059e..a955622 100644 --- a/app/routes/vault.py +++ b/app/routes/vault.py @@ -1,5 +1,5 @@ from flask import Blueprint, request, jsonify, g -from app import db +from app import db, limiter from app.models.vault_item import VaultItem, ItemType from app.models.audit_log import AuditLog from app.services.auth_service import require_jwt @@ -15,6 +15,7 @@ def _client_ip(): @vault_bp.route('', methods=['GET']) +@limiter.limit('120 per minute') @require_jwt def list_items(): items = VaultItem.query.filter_by(user_id=g.current_user_id).order_by( @@ -24,6 +25,7 @@ def list_items(): @vault_bp.route('', methods=['POST']) +@limiter.limit('60 per minute') @require_jwt def create_item(): data = request.get_json(silent=True) or {} @@ -72,6 +74,7 @@ def create_item(): @vault_bp.route('/', methods=['GET']) +@limiter.limit('120 per minute') @require_jwt def get_item(item_id): item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first() @@ -81,6 +84,7 @@ def get_item(item_id): @vault_bp.route('/', methods=['PUT']) +@limiter.limit('60 per minute') @require_jwt def update_item(item_id): item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first() @@ -129,6 +133,7 @@ def update_item(item_id): @vault_bp.route('/', methods=['DELETE']) +@limiter.limit('60 per minute') @require_jwt def delete_item(item_id): item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first() @@ -153,6 +158,7 @@ def delete_item(item_id): # ── Import / Export ─────────────────────────────────────────────────────────── @vault_bp.route('/export', methods=['GET']) +@limiter.limit('10 per minute') @require_jwt def export_items(): """ @@ -178,6 +184,7 @@ def export_items(): @vault_bp.route('/import', methods=['POST']) +@limiter.limit('10 per minute') @require_jwt def import_items(): """