From 7225a8df4efed55ad4b4595699e9f02e489acaad Mon Sep 17 00:00:00 2001 From: saaa99999999 Date: Sun, 24 May 2026 22:48:25 +0800 Subject: [PATCH 1/2] Add authentication and IDOR protection to account endpoints All four account CRUD endpoints (GET /accounts, GET /accounts/{id}, PATCH /accounts/{id}, DELETE /accounts) had no authentication, allowing unauthenticated attackers to enumerate all users with their emails, modify any user's password/email, and delete any account. Added get_current_user dependency that validates JWT Bearer tokens and added IDOR checks to ensure users can only access and modify their own account data. --- backend/src/api/dependencies/auth.py | 29 ++++++++++++++ backend/src/api/routes/account.py | 57 +++++++++++++++++----------- 2 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 backend/src/api/dependencies/auth.py diff --git a/backend/src/api/dependencies/auth.py b/backend/src/api/dependencies/auth.py new file mode 100644 index 0000000..56fe1fd --- /dev/null +++ b/backend/src/api/dependencies/auth.py @@ -0,0 +1,29 @@ +import fastapi +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +from src.api.dependencies.repository import get_repository +from src.config.manager import settings +from src.repository.crud.account import AccountCRUDRepository +from src.securities.authorizations.jwt import jwt_generator +from src.utilities.exceptions.database import EntityDoesNotExist +from src.utilities.exceptions.http.exc_401 import http_exc_401_cunauthorized_request + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = fastapi.Depends(HTTPBearer()), + account_repo: AccountCRUDRepository = fastapi.Depends(get_repository(repo_type=AccountCRUDRepository)), +): + try: + details = jwt_generator.retrieve_details_from_token( + token=credentials.credentials, + secret_key=settings.JWT_SECRET_KEY, + ) + except (ValueError, Exception): + raise await http_exc_401_cunauthorized_request() + + username = details[0] + try: + db_account = await account_repo.read_account_by_username(username=username) + except EntityDoesNotExist: + raise await http_exc_401_cunauthorized_request() + return db_account diff --git a/backend/src/api/routes/account.py b/backend/src/api/routes/account.py index f3cfb1d..979f333 100644 --- a/backend/src/api/routes/account.py +++ b/backend/src/api/routes/account.py @@ -1,11 +1,16 @@ import fastapi import pydantic +from src.api.dependencies.auth import get_current_user from src.api.dependencies.repository import get_repository +from src.models.db.account import Account from src.models.schemas.account import AccountInResponse, AccountInUpdate, AccountWithToken from src.repository.crud.account import AccountCRUDRepository from src.securities.authorizations.jwt import jwt_generator from src.utilities.exceptions.database import EntityDoesNotExist +from src.utilities.exceptions.http.exc_403 import ( + http_403_exc_forbidden_request, +) from src.utilities.exceptions.http.exc_404 import ( http_404_exc_email_not_found_request, http_404_exc_id_not_found_request, @@ -22,29 +27,24 @@ status_code=fastapi.status.HTTP_200_OK, ) async def get_accounts( + current_user: Account = fastapi.Depends(get_current_user), account_repo: AccountCRUDRepository = fastapi.Depends(get_repository(repo_type=AccountCRUDRepository)), ) -> list[AccountInResponse]: - db_accounts = await account_repo.read_accounts() - db_account_list: list = list() - - for db_account in db_accounts: - access_token = jwt_generator.generate_access_token(account=db_account) - account = AccountInResponse( - id=db_account.id, - authorized_account=AccountWithToken( - token=access_token, - username=db_account.username, - email=db_account.email, # type: ignore - is_verified=db_account.is_verified, - is_active=db_account.is_active, - is_logged_in=db_account.is_logged_in, - created_at=db_account.created_at, - updated_at=db_account.updated_at, - ), - ) - db_account_list.append(account) - - return db_account_list + access_token = jwt_generator.generate_access_token(account=current_user) + account = AccountInResponse( + id=current_user.id, + authorized_account=AccountWithToken( + token=access_token, + username=current_user.username, + email=current_user.email, # type: ignore + is_verified=current_user.is_verified, + is_active=current_user.is_active, + is_logged_in=current_user.is_logged_in, + created_at=current_user.created_at, + updated_at=current_user.updated_at, + ), + ) + return [account] @router.get( @@ -55,8 +55,12 @@ async def get_accounts( ) async def get_account( id: int, + current_user: Account = fastapi.Depends(get_current_user), account_repo: AccountCRUDRepository = fastapi.Depends(get_repository(repo_type=AccountCRUDRepository)), ) -> AccountInResponse: + if id != current_user.id: + raise await http_403_exc_forbidden_request() + try: db_account = await account_repo.read_account_by_id(id=id) access_token = jwt_generator.generate_access_token(account=db_account) @@ -87,11 +91,15 @@ async def get_account( ) async def update_account( query_id: int, + current_user: Account = fastapi.Depends(get_current_user), update_username: str | None = None, update_email: pydantic.EmailStr | None = None, update_password: str | None = None, account_repo: AccountCRUDRepository = fastapi.Depends(get_repository(repo_type=AccountCRUDRepository)), ) -> AccountInResponse: + if query_id != current_user.id: + raise await http_403_exc_forbidden_request() + account_update = AccountInUpdate(username=update_username, email=update_email, password=update_password) try: updated_db_account = await account_repo.update_account_by_id(id=query_id, account_update=account_update) @@ -118,8 +126,13 @@ async def update_account( @router.delete(path="", name="accountss:delete-account-by-id", status_code=fastapi.status.HTTP_200_OK) async def delete_account( - id: int, account_repo: AccountCRUDRepository = fastapi.Depends(get_repository(repo_type=AccountCRUDRepository)) + id: int, + current_user: Account = fastapi.Depends(get_current_user), + account_repo: AccountCRUDRepository = fastapi.Depends(get_repository(repo_type=AccountCRUDRepository)), ) -> dict[str, str]: + if id != current_user.id: + raise await http_403_exc_forbidden_request() + try: deletion_result = await account_repo.delete_account_by_id(id=id) From 1f27e031bf7ea1c27b02ff6ec8e722972826e90d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 14:49:52 +0000 Subject: [PATCH 2/2] ci(pre-commit): Autofixing commit msg from pre-commit.com hooks --- backend/src/api/routes/account.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/api/routes/account.py b/backend/src/api/routes/account.py index 979f333..d426172 100644 --- a/backend/src/api/routes/account.py +++ b/backend/src/api/routes/account.py @@ -8,9 +8,7 @@ from src.repository.crud.account import AccountCRUDRepository from src.securities.authorizations.jwt import jwt_generator from src.utilities.exceptions.database import EntityDoesNotExist -from src.utilities.exceptions.http.exc_403 import ( - http_403_exc_forbidden_request, -) +from src.utilities.exceptions.http.exc_403 import http_403_exc_forbidden_request from src.utilities.exceptions.http.exc_404 import ( http_404_exc_email_not_found_request, http_404_exc_id_not_found_request,