Безопасность¶
Безопасность — это набор умолчаний, а не чек-лист, прикручиваемый потом. Каждый пункт ниже либо fail-closed автоматически, либо ставится одной явной строкой — ничего из этого не требует security-команды, чтобы включить.
Страница организована в три части: что фреймворк уже защищает сам (ничего настраивать не нужно), набор middleware для усиления, который вы сами подключаете под свой конкретный деплой, и горстка вещей, которые принципиально ваше решение, потому что у фреймворка нет способа узнать ваше намерение.
Встроено — настраивать не нужно¶
SQL-инъекции¶
- Значения всегда биндятся драйвером — никогда не подставляются в SQL строками.
- Идентификаторы (таблицы/колонки/алиасы) валидируются (
^[A-Za-z_]\w*$) и квотируются; всё прочее вызываетUnsafeIdentifierError. - Lookup'ы — строгий белый список: неизвестный lookup вызывает исключение.
- Wildcards
LIKEв пользовательском вводе экранируются черезESCAPE. LIMIT/OFFSETприводятся к целым числам.
Доказано, а не просто заявлено
Тестовый набор включает явные тесты на инъекции, доказывающие, что
враждебный ввод остаётся связанным параметром — см. ORM.
CI также прогоняет bandit по SQL-строящему коду ORM на каждый push (см.
раздел «Непрерывное сканирование» ниже).
Инъекция через заголовки и cookies ответа¶
Response отклоняет (бросает ValueError) любое имя заголовка, значение
заголовка, компонент cookie или media_type, содержащий сырой CR, LF или
NUL — классический примитив "HTTP response splitting" (CWE-113). Проверяется
безусловно в момент построения ответа — и для Response, и для
StreamingResponse — а не оставляется на откуп ASGI-серверу.
Маскирование логов и repr¶
- Логирующий middleware маскирует чувствительные ключи до записи — см.
Логирование. Сравнение по подстроке, без учёта регистра и
разделителей, так что
old_password,X-Api-Keyиrefresh_tokenтоже ловятся, а не только поле, буквально названноеpassword. Model.__repr__маскирует поля, чьё имя похоже на секрет, точно так же — колонкаpassword_hashне окажется читаемой в случайномprint(user), трейсбеке или дебаггере.
Шифрование файлов на диске¶
FileField шифрует загрузки AES-256-GCM; утёкшая папка хранилища невосстановима
без отдельного ключа, а подмена данных обнаруживается. См.
Шифрованные файлы.
Cookies и CSRF¶
set_signed_cookie/get_signed_cookieиспользуют HMAC-SHA256, так что cookies нельзя подделать.csrf_middlewareреализует паттерн signed double-submit-cookie для небезопасных методов, сравнивая cookie и заголовок за константное время.- Cookies по умолчанию
SameSite=Lax; при необходимости ставьтеsecure=True,httponly=True.
Сессии и аутентификация¶
Встроено, только stdlib. Сессия целиком едет в HMAC-подписанной куке (stateless,
без серверного хранилища); пароли хешируются scrypt (hashlib.scrypt) в
самоописывающем формате — параметры стойкости можно поднять позже.
# Middleware/__init__.py
from endocore.middleware import session_middleware
middlewares = [session_middleware(secret=env("SECRET_KEY"), secure=True)]
# Api/v1/Login/Post.py
from endocore import Response, login, verify_password
from Models.user import User
async def handler(request):
body = await request.json()
user = await User.objects.filter(email=body["email"]).afirst()
# None тоже сжигает полную scrypt-деривацию: неизвестный email отвечает
# так же долго, как неверный пароль — перечислить аккаунты по таймингу нельзя.
if not verify_password(body["password"], user.password_hash if user else None):
return Response.json({"error": "invalid credentials"}, status=401)
login(request, user.pk) # кладёт pk в сессию
return Response.json({"ok": True})
# Api/v1/Me/Get.py — 401 анонимным запросам, через DI
from endocore import Depends, Response, require_user_id
async def handler(request, user_id = Depends(require_user_id)):
return Response.json({"user_id": user_id})
hash_password(pw)→ сохраните строку;verify_password(pw, stored)— сравнение за константное время;needs_rehash(stored)подскажет, когда перехешировать после логина.login(request, pk)/logout(request)/user_id(request)(→ pk илиNone).request.session— обычный dict; кука перезаписывается только если сессию меняли, и удаляется при очистке. Держите её маленькой (лимит куки ~4 КБ).- Подделанная/протухшая кука сессии даёт чистую анонимную сессию, а не 500.
WebSockets (cross-site hijacking)¶
Хендшейк по умолчанию требует совпадения origin вне dev=True — страница на
чужом сайте, открывающая websocket к вашему приложению, не может прокатиться
на cookie-сессии просто потому, что браузер прикрепляет cookies к соединению
(cross-site websocket hijacking). Настройте явно для реального кросс-origin
фронтенда, в том же виде, что и cors_middleware:
ws_allowed_origins="*" отключает проверку; если не задавать вовсе, она сама
ослабляется в dev=True (локальный фронтенд на другом порту — уже другой
origin, и same-origin иначе отклонял бы каждое локальное dev-соединение).
Supply chain API-документации (/docs)¶
Страница Swagger UI загружает CSS/JS с закреплённой версии swagger-ui-dist
с хешем Subresource Integrity — браузер откажется выполнять скрипт, если CDN
вдруг отдаст что-то не совпадающее с хешем, так что скомпрометированный edge
CDN или угнанный npm-релиз не смогут незаметно подсунуть JS на страницу
документации. /docs и /openapi.json к тому же по умолчанию отдаются
только в dev=True — см. Развёртывание.
Лимит размера тела¶
Приложение отклоняет тела запросов больше max_body_size (по умолчанию 16 МБ)
со статусом 413 — защита от загрузок, исчерпывающих память.
Некорректный ввод падает чисто, а не громко
Не-UTF-8 поле формы, некорректная multipart-граница или патологически
глубоко вложенный JSON — всё это резолвится в чистый 400 Bad Request,
найдено и закреплено фаззингом парсеров через hypothesis (см. раздел
«Непрерывное сканирование» ниже), а не всплывает необработанной 500-й.
Набор для усиления — подключайте то, что нужно вашему деплою¶
Ничто из этого не включено по умолчанию, потому что фреймворк не может знать вашу топологию (есть ли обратный прокси? один вызывающий или весь интернет? общий ли Redis?). Каждое — одна строка.
from endocore.middleware import (
security_headers_middleware, cors_middleware, rate_limit_middleware,
proxy_headers_middleware, timeout_middleware, csrf_middleware,
ip_allowlist_middleware,
)
middlewares = [
security_headers_middleware(hsts=True), # nosniff, DENY frames, HSTS
cors_middleware(allow_origins=["https://app.example.com"]),
ip_allowlist_middleware(allowed=["203.0.113.7"]), # только если вызывающий один известный
rate_limit_middleware(limit=100, window=60),
proxy_headers_middleware(trusted=["10.0.0.1"]), # X-Forwarded-* только от этих
timeout_middleware(seconds=30),
csrf_middleware(secret="…"),
]
Белый список IP — ограничьте бэкенд одним известным вызывающим (фронтендом, внутренней сетью) по source IP/CIDR, а не доверяя заголовку, в котором вызывающий может просто соврать:
from endocore.middleware import ip_allowlist_middleware
middlewares = [ip_allowlist_middleware(allowed=["203.0.113.7", "10.0.0.0/24"])]
Поставьте proxy_headers_middleware первым, если запросы идут через обратный
прокси, иначе каждый запрос будет нести IP прокси, а не реального клиента.
Подпись кэша — значения RedisCache сериализуются pickle; pickle.loads()
над тем, что вернул Redis, безопасен ровно настолько, насколько безопасен сам
Redis. Передайте secret=, чтобы подписывать каждое значение HMAC (привязанным
к его ключу кэша, так что подписанный блоб нельзя скопировать на другой ключ и
пройти проверку), и трактовать неподписанное или подделанное как промах кэша,
а не десериализовывать его:
Без secret= значения остаются неподписанными (как раньше), и выводится
предупреждение.
Знайте границы — не баги, а острые края¶
timeout_middleware не останавливает синхронные хендлеры
Отмена доходит до ближайшего await в асинхронном хендлере и реально
его останавливает. Синхронный хендлер выполняется в рабочем потоке —
Python не может принудительно убить работающий поток, поэтому он
продолжает работать до конца в общем пуле потоков даже после того, как
клиент уже получил свой 504. Достаточное число медленных синхронных
хендлеров всё ещё может исчерпать этот пул и застопорить несвязанные
запросы, при том что каждый отдельный ответ выглядит нормально.
Ограничивайте и саму работу (таймаут запроса к БД, таймаут HTTP-клиента) —
не полагайтесь только на этот middleware для синхронного кода.
Сжатие + секреты (BREACH)
gzip_middleware сжимает любой ответ больше minimum_size, не зная, что
в нём. Ответ, в котором в одном теле оказались и секрет (CSRF-токен, id
сессии), и отражённый пользовательский ввод (эхо query-параметра) —
компрессионный оракл вне зависимости от того, чем именно его сжали.
Держите их порознь, либо не сжимайте страницы, где встречается секрет.
Риски уровня приложения, которые фреймворк не может решить за вас¶
- Mass assignment —
Model(**body)/Model.objects.create(**body)устанавливает каждый переданный ключ, включая те, что вы не собирались открывать (is_staff,pk, ...). Фреймворк не может угадать, какие поля конкретный endpoint должен принимать от клиента; собирайте dict из явных именованных полей (Model(name=body["name"])), а не разворачивайте тело запроса напрямую — как и с любой другой ORM. - Open redirect —
Response.redirect(location)отправляет ровно тотlocation, что вы передали. Если он берётся из пользовательского ввода (?next=), проверьте, что это путь того же сайта, прежде чем редиректить — если только внешний редирект не является намеренным поведением (например, OAuth callback). - Path-параметры в путях файлов — захваченный сегмент (
[id]) — это просто URL-декодированная строка; роутер не проверяет её содержимое (ему достаточно, что она матчит динамический сегмент пути).open(f"uploads/{id}"), построенный из неё напрямую, — path-traversal баг (id="../../etc/passwd"), как и везде, где пользовательский ввод попадает в файловый вызов — используйте хранилищеFileFieldлибо проверяйте значение по белому списку/ известному пространству ID сами.
Непрерывное сканирование¶
Каждый push и PR запускает отдельную CI-джобу security рядом с матрицей
тестов:
bandit— статический анализ поendocore/. Каждая существующая находка — либо путь HMAC-подписанного pickle в кэше, либо SQL-строящий слой ORM (идентификаторы квотируются, значения всегда биндятся — размечено построчным# nosecс причиной, а не общим отключением проверки); новый паттерн сырого SQL или pickle где-либо ещё по-прежнему валит сборку.pip-audit— проверяет реальное дерево зависимостей EndoCore (замороженное сразу после установки, до добавления в то же окружение самих сканеров) на известные CVE.
Парсеры (multipart, JSON-тело, query-строка) дополнительно
property-фаззились через hypothesis в рамках аудита, породившего эту
страницу — каждый найденный краш исправлен и закреплён regression-тестом.
Чек-лист для продакшена¶
- [ ] Задайте сильный секрет для подписанных cookies / CSRF (из env, не в коде).
- [ ]
configure_storage(key=…)из секрет-менеджера; сделайте бэкап ключа. - [ ] Включите
security_headers_middleware(hsts=True)за TLS. - [ ] Ограничьте
cors_middleware(allow_origins=[…])своими фронтендами. - [ ] Задайте
ws_allowed_origins=[…], если какой-то websocket-endpoint читает сессию. - [ ] Поставьте
proxy_headers_middleware(trusted=[…]), если вы за балансировщиком. - [ ] Добавьте
ip_allowlist_middleware, если к API должен обращаться только один вызывающий. - [ ] Добавьте
rate_limit_middleware(или лимитер на Redis) на публичные маршруты. - [ ] Передайте
secret=вCacheExtension(backend="redis", ...), если этот Redis не полностью доверенный. - [ ] Работайте по HTTPS; терминируйте TLS на прокси.