From 7e59c1e5b0c8637c490b38e00f0941041ed3aef2 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 30 Aug 2026 14:53:41 +0500 Subject: [PATCH] =?UTF-8?q?fix(#3194):=20hide=5Fparameters=3DTrue=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B2=D1=81=D0=B5=D1=85=20=D0=B4=D0=B2=D0=B8=D0=B6?= =?UTF-8?q?=D0=BA=D0=B0=D1=85,=20include=5Flocal=5Fvariables=3DFalse=20?= =?UTF-8?q?=D1=83=20scheduler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ключ шифрования кук и сами куки уезжали в GlitchTip: сервисы сессий передают их bind-параметрами в pgp_sym_encrypt(:cookies_json, :key), а SQLAlchemy при ошибке печатает ВСЕ параметры в тексте StatementError. Правка на уровне движка (backend + tradein-mvp: db.py, auth_db.py, alembic/env.py) кроет все сайты вызова разом, включая четвёртую копию в scraper-kit и всё будущее. scheduler_main.py был единственным из трёх sentry_sdk.init без include_local_variables=False — процесс скрейпера, в кадрах лежат прокси-креды. НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением) и сырые psycopg-подключения мимо движков — отдельный класс. Co-Authored-By: Claude Opus 5 --- backend/alembic/env.py | 6 +++ backend/app/core/auth_db.py | 7 +++ backend/app/core/db.py | 13 ++++- tradein-mvp/backend/app/core/auth_db.py | 13 ++++- tradein-mvp/backend/app/core/db.py | 13 ++++- tradein-mvp/backend/app/scheduler_main.py | 5 ++ .../tests/test_3194_hide_sql_parameters.py | 47 +++++++++++++++++++ 7 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 tradein-mvp/backend/tests/test_3194_hide_sql_parameters.py diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 09bd58e7..3864832c 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -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( diff --git a/backend/app/core/auth_db.py b/backend/app/core/auth_db.py index ffde543a..12e3e8d6 100644 --- a/backend/app/core/auth_db.py +++ b/backend/app/core/auth_db.py @@ -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 доходит до diff --git a/backend/app/core/db.py b/backend/app/core/db.py index 7853cad9..cd18b717 100644 --- a/backend/app/core/db.py +++ b/backend/app/core/db.py @@ -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) diff --git a/tradein-mvp/backend/app/core/auth_db.py b/tradein-mvp/backend/app/core/auth_db.py index 0b87bc54..926c6912 100644 --- a/tradein-mvp/backend/app/core/auth_db.py +++ b/tradein-mvp/backend/app/core/auth_db.py @@ -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'`, diff --git a/tradein-mvp/backend/app/core/db.py b/tradein-mvp/backend/app/core/db.py index 7853cad9..cd18b717 100644 --- a/tradein-mvp/backend/app/core/db.py +++ b/tradein-mvp/backend/app/core/db.py @@ -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) diff --git a/tradein-mvp/backend/app/scheduler_main.py b/tradein-mvp/backend/app/scheduler_main.py index 210b30fa..59254465 100644 --- a/tradein-mvp/backend/app/scheduler_main.py +++ b/tradein-mvp/backend/app/scheduler_main.py @@ -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=[ diff --git a/tradein-mvp/backend/tests/test_3194_hide_sql_parameters.py b/tradein-mvp/backend/tests/test_3194_hide_sql_parameters.py new file mode 100644 index 00000000..dc7aa623 --- /dev/null +++ b/tradein-mvp/backend/tests/test_3194_hide_sql_parameters.py @@ -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