#3194: ключ шифрования кук и сами куки больше не уезжают в GlitchTip #3275

Merged
bot-backend merged 2 commits from fix/3194-hide-sql-parameters into main 2026-08-30 10:42:01 +00:00
7 changed files with 101 additions and 3 deletions

View file

@ -43,6 +43,12 @@ def run_migrations_online() -> None:
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
# #3194: SQLAlchemy печатает ВСЕ bind-параметры в тексте StatementError.
# Миграции гоняют DDL/DML с литералами и параметрами из данных — флаг
# на уровне движка не даёт им уехать в GlitchTip.
# НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со
# значением) и сырые psycopg-подключения мимо движков.
hide_parameters=True,
)
with connectable.connect() as connection:
context.configure(

View file

@ -139,6 +139,13 @@ def _build() -> tuple[Engine, sessionmaker[Session]]:
future=True,
pool_timeout=3,
connect_args={"connect_timeout": 3, "options": "-c statement_timeout=3000"},
# #3194: SQLAlchemy печатает ВСЕ bind-параметры в тексте StatementError —
# через них в GlitchTip уезжали ключ шифрования кук и сами куки
# (pgp_sym_encrypt(:cookies_json, :key)). Флаг на УРОВНЕ ДВИЖКА кроет все
# сайты вызова разом, включая будущие.
# НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением)
# и сырые psycopg-подключения мимо движков — это отдельный класс.
hide_parameters=True,
)
except (ArgumentError, ValueError):
# ValueError — не паранойя: на «почти URL» разбор SQLAlchemy доходит до

View file

@ -5,7 +5,18 @@ from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from app.core.config import settings
engine = create_engine(settings.database_url, pool_pre_ping=True, future=True)
engine = create_engine(
settings.database_url,
pool_pre_ping=True,
future=True,
# #3194: SQLAlchemy печатает ВСЕ bind-параметры в тексте StatementError —
# через них в GlitchTip уезжали ключ шифрования кук и сами куки
# (pgp_sym_encrypt(:cookies_json, :key)). Флаг на УРОВНЕ ДВИЖКА кроет все
# сайты вызова разом, включая будущие.
# НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением)
# и сырые psycopg-подключения мимо движков — это отдельный класс.
hide_parameters=True,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)

View file

@ -90,7 +90,18 @@ def _build() -> tuple[Engine, sessionmaker[Session]]:
if not dsn:
raise AuthDatabaseNotConfiguredError(_NOT_CONFIGURED_MSG)
try:
engine = create_engine(dsn, pool_pre_ping=True, future=True)
engine = create_engine(
dsn,
pool_pre_ping=True,
future=True,
# #3194: SQLAlchemy печатает ВСЕ bind-параметры в тексте StatementError —
# через них в GlitchTip уезжали ключ шифрования кук и сами куки
# (pgp_sym_encrypt(:cookies_json, :key)). Флаг на УРОВНЕ ДВИЖКА кроет все
# сайты вызова разом, включая будущие.
# НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением)
# и сырые psycopg-подключения мимо движков — это отдельный класс.
hide_parameters=True,
)
except (ArgumentError, ValueError):
# ValueError — не паранойя: на «почти URL» разбор SQLAlchemy доходит до
# `int(port)` и падает с `invalid literal for int() with base 10: 'w'`,

View file

@ -5,7 +5,18 @@ from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from app.core.config import settings
engine = create_engine(settings.database_url, pool_pre_ping=True, future=True)
engine = create_engine(
settings.database_url,
pool_pre_ping=True,
future=True,
# #3194: SQLAlchemy печатает ВСЕ bind-параметры в тексте StatementError —
# через них в GlitchTip уезжали ключ шифрования кук и сами куки
# (pgp_sym_encrypt(:cookies_json, :key)). Флаг на УРОВНЕ ДВИЖКА кроет все
# сайты вызова разом, включая будущие.
# НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением)
# и сырые psycopg-подключения мимо движков — это отдельный класс.
hide_parameters=True,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, expire_on_commit=False)

View file

@ -80,6 +80,11 @@ if settings.glitchtip_dsn:
release=os.getenv("GIT_SHA") or os.getenv("SENTRY_RELEASE") or "unknown",
traces_sample_rate=0.0,
send_default_pii=False,
# #3194: процесс скрейпера — в локальных переменных кадров лежат
# прокси-креды вида user:pass, default sentry_sdk (True) приложил бы их
# к traceback открытым текстом. Так же выставлено у соседей (main.py,
# tgbot). НЕ закрывает значения в самом тексте исключения и в логах.
include_local_variables=False,
before_send=_before_send,
before_send_transaction=_before_send,
integrations=[

View file

@ -0,0 +1,47 @@
"""#3194: bind-параметры не должны попадать в текст исключения (→ GlitchTip).
Через них уезжали ключ шифрования кук и сами куки: сервисы сессий зовут
`db.execute(text("... pgp_sym_encrypt(:cookies_json, :key) ..."), {...})`, а
SQLAlchemy при ошибке оборачивает всё в StatementError и печатает ВСЕ
параметры в `str(exc)`.
Отказ здесь обязан произойти НА УРОВНЕ ЗАПРОСА: если соединение падает на
CONNECT, до bind-параметров дело не доходит и тест зелёный по построению.
Поэтому живой sqlite в памяти и обращение к несуществующей таблице.
"""
from __future__ import annotations
import pytest
from sqlalchemy import create_engine, text
from sqlalchemy.exc import StatementError
SECRET = "sup3r-secret-cookie-key-3194"
def _provoke_statement_error(*, hide: bool) -> str:
engine = create_engine("sqlite://", hide_parameters=hide)
with engine.connect() as conn:
with pytest.raises(StatementError) as excinfo:
conn.execute(text("SELECT :key FROM no_such_table_3194"), {"key": SECRET})
return str(excinfo.value)
def test_bind_parameters_hidden_in_exception_text() -> None:
message = _provoke_statement_error(hide=True)
# Отказ именно на запросе, а не на соединении — иначе проверка ниже пуста.
assert "no_such_table_3194" in message
assert SECRET not in message
def test_falsification_secret_leaks_without_the_flag() -> None:
"""Обратная сторона: без флага секрет ВИДЕН — тест выше не тавтология."""
message = _provoke_statement_error(hide=False)
assert SECRET in message
def test_app_engine_hides_parameters() -> None:
"""Сам прод-движок, а не только принцип: create_engine ленив, коннекта нет."""
from app.core.db import engine
assert engine.hide_parameters is True