Код-ревью Middle Аутентификация и права спрашивают часто обновлено

Код-ревью эндпоинта входа и проверки токена

Вопрос

Найдите уязвимости в коде входа и проверки API-ключа.

import hashlib
import jwt
from fastapi import FastAPI, HTTPException, Header

app = FastAPI()
SECRET = 'secret123'


@app.post('/login')
def login(email: str, password: str):
    user = db.find_user(email)
    if user is None:
        raise HTTPException(404, 'User not found')
    if user.password != hashlib.md5(password.encode()).hexdigest():
        raise HTTPException(401, 'Wrong password')
    token = jwt.encode({'sub': user.id, 'role': user.role}, SECRET)
    return {'token': token}


@app.get('/admin/report')
def report(x_api_key: str = Header()):
    if x_api_key == ADMIN_KEY:
        return build_report()
    raise HTTPException(403)


def check(token: str):
    return jwt.decode(token, SECRET, options={'verify_signature': False})

Коротко

Исправления: секрет из окружения; пароль через argon2.PasswordHasher().verify(user.passwordhash, password) с общим ответом «неверный email или пароль» и ограничением попыток; токен с exp, iat и типом: Проверка всегда с подписью и списком алгоритмов: jwt.decode(token, key, algorithms=["HS256"]).

Что должен найти ревьюер

  1. Безопасностьстрока 6
    Слабый секрет прямо в коде; секрет из переменных окружения, длинный и случайный
  2. Безопасностьстрока 13
    Разные ответы для «нет пользователя» и «неверный пароль» позволяют перебирать email; нужен общий ответ 401
  3. Безопасностьстрока 14
    MD5 без соли для паролей: быстро перебирается; нужен Argon2 или bcrypt (и ограничение попыток)
  4. Безопасностьстрока 16
    JWT без exp: токен бессрочный и неотзываемый; добавить короткий срок и refresh
  5. Безопасностьстрока 22
    Сравнение секрета через == подвержено атаке по времени; hmac.compare_digest
  6. Безопасностьстрока 28
    Проверка подписи отключена: любой может подделать токен с ролью admin

Разбор

Исправления: секрет из окружения; пароль через argon2.PasswordHasher().verify(user.password_hash, password) с общим ответом «неверный email или пароль» и ограничением попыток; токен с exp, iat и типом:

now = int(time.time())
token = jwt.encode({'sub': str(user.id), 'exp': now + 900, 'iat': now, 'type': 'access'},
                   settings.jwt_secret, algorithm='HS256')

Проверка всегда с подписью и списком алгоритмов: jwt.decode(token, key, algorithms=["HS256"]). API-ключ сравнивается через hmac.compare_digest, а лучше хранится в базе как хеш. Роль в токене удобна, но после смены роли старые токены ещё живут до истечения exp.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.

Термины по теме