DarkRiDDeR12 мин

Старый session ID после rotation: маршрут проверки logout без выдуманного trace

БезопасностьАутентификацияПрактика

Симптом часто звучит так: «в одной вкладке вышли, в другой ещё есть доступ» или «после renewal разлогинило». Из этого не видно, какой ID пришёл на сервер, какой был current и какой logout scope выбран. Цена правки наугад — старая сессия остаётся принятой либо поздний logout закрывает новую.

Ниже — маршрут, не отчёт об инциденте: нет пользователя, вкладок, Network/Storage или HTTP-запуска. Есть fixture одной lineage: fixture-s-1 становится rotated, fixture-s-2 active, старый ID stale, logout нового ID revoke-ит его. Это задаёт вопросы для реального расследования, не выдумывает trace.

Соберите четыре факта до изменения флага

Начинать стоит с формата записи: безопасный surrogate ID, server-side status в момент обработки, lineage/current ID и тип операции. Полный bearer secret в лог не попадает. Нужна корреляция, по которой reviewer сопоставит request с решением, но не повторит сессию.

Отделяйте факт от интерпретации. «Cookie не удалена» требует разрешённой проверки клиента; «logout не дошёл» — evidence запроса или server record; «старый ID прошёл» — статус именно старого ID. Если данных нет, причина не подтверждена: fixture подсказывает инвариант, не историю браузера.

Симптом → причина → проверка → действие для одной lineage
СимптомВероятная причинаПроверка без секретаДействие
После renewal старый ID ещё выполняет защищённое действиеrecord не стал rotated или handler не проверяет currentсопоставить безопасный ID записи, status и lineage pointer в разрешённой средеотклонять rotated ID до эффекта и линейризовать rotation
Logout меняет UI, но следующий запрос приняточищена только cookie или revoke не дошёл до server recordпроверить audit event logout и status того же recordrevoke server record и вернуть clear cookie одинакового scope
Поздний logout отключает новую сессиюне определён scope, stale ID трактуется как currentсравнить предъявленный ID с currentByLineage на момент обработкивыбрать явный logout-current или logout-lineage и добавить invariant
После clear cookie видна другая cookie того же имениissue/clear расходятся в Path или Domainсравнить атрибуты Set-Cookie без значения bearer IDудалять cookie теми же name, Path и Domain при наличии
Проблема есть только в одном браузереполитика хранения или порядок ответов отличаетсяповторить разрешённый минимальный сценарий на конкретной версиине менять server contract до подтверждения различия

Старый ID — это не обязательно неизвестный ID

Различайте unknown, rotated и revoked. Unknown — store не нашёл ID; rotated — запись известна, но не current и имеет successor; revoked — сессия завершена. Внешне они могут дать один 401, но внутри должны отличаться, иначе нельзя доказать, что rotation вывела старый ID из обращения.

Fixture хранит predecessor ради различия, не как совет хранить records вечно. На период возможных повторов сервер должен отклонять старый ID. RFC 6265 задаёт browser scope доставки; прикладную семантику Cookie определяет сервер.

Воспроизводимый fixture и invariants stale событий

Fixture sidecar создаёт current record, делает accepted rotation, затем повторяет rotate/logout со старым ID. Обе stale операции возвращают accepted=false и оставляют successor active. Logout current ID делает record revoked, удаляет pointer и формирует clear cookie. Есть и положительная, и отрицательная ветви.

import {
  createTeachingSessionState,
  rotateTeachingSession,
  logoutTeachingSession,
} from './upgrade-2023-02.mjs';

const first = createTeachingSessionState();
const replacement = rotateTeachingSession(first, 'fixture-s-1');
const staleRotation = rotateTeachingSession(replacement.state, 'fixture-s-1');
const staleLogout = logoutTeachingSession(replacement.state, 'fixture-s-1');

if (staleRotation.accepted || staleLogout.accepted) {
  throw new Error('old ID changed current session');
}
if (replacement.state.records['fixture-s-2'].status !== 'active') {
  throw new Error('successor is not active');
}

Команда node web/scripts/upgrade-2023-02.mjs --verify-fixture запускает полный набор assertions скрипта. Он воспроизводим только потому, что значения и порядок вызовов фиксированы. Его PASS не подтверждает, что web server выдал Set-Cookie, что браузер применил ответ, что база сделала compare-and-swap или что logout endpoint защищён от подделки. В review укажите этот предел рядом с результатом. Иначе маленький тест станет ложным evidence для нескольких неиспытанных уровней системы.

Маршрут stale событий в учебной lineage: rotation заменяет fixture-s-1 на fixture-s-2, повторная rotation и поздний logout со старым ID получают reject и не меняют successor, logout текущего ID revoke-ит fixture-s-2 и выдаёт clear cookie.
Схема задаёт проектное правило для одного ID и синхронных вызовов в памяти. Она не является browser trace, журналом реального инцидента или доказательством порядка HTTP-ответов.

Порядок ответа и порядок решения — разные задачи

Даже с правильным server reject клиент может получить ответы в неудобном порядке. Два запроса ушли со старым ID, один принёс новый Set-Cookie, другой ошибку. Browser cookie jar и fetch/navigation имеют свои правила; fixture их не моделирует. Реальный дефект требует отдельного разрешённого repro с версиями, шагами, маскированными значениями и HTTP-метаданными.

Серверный инвариант остаётся: принимается только current active ID; stale событие не создаёт successor и не revoke-ит нового при logout-current. UX после reject выбирает продукт. Не прячьте его бесконечным retry без определения перехода пользователя.

Проверьте clear cookie как договор, а не как строку в шаблоне

Удаление cookie требует того же имени и scope, что issue. В модели clear cookie: __Host-session, Path=/, Secure, HttpOnly, SameSite=Lax и Expires в прошлом. __Host- исключает Domain, поэтому issue/clear не расходятся по поддоменному scope. Если Domain нужен в другом контракте, он обязан быть в документе и проверке очистки; не добавляйте его только на logout.

Flags clear cookie не отменяют server revoke. RFC 6265 связывает замену с name, domain и path и не советует полагаться на порядок одинаковых Cookie. При нескольких Set-Cookie нужен один owner, один scope и проверка отсутствия дубликата в разрешённом сценарии.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. Запишите наблюдаемое последствие без вывода: какой экран, какой внешний status, какая безопасная корреляция операции. Не называйте браузерный trace, если его не собирали.
  2. Причина. Проверьте гипотезы по очереди: server record не изменился, handler не сверил current, stale logout имеет неясный scope или issue/clear cookies различаются по атрибутам.
  3. Проверка модели. Выполните fixture и прочитайте assertions staleRotationDoesNotCreateAnotherSession и staleLogoutDoesNotRevokeSuccessor. Это проверка выбранного правила, не production доказательство.
  4. Проверка реализации. В разрешённой среде соберите ID surrogate, record status, lineage pointer и Set-Cookie attributes без bearer значения. Сверьте момент обработки, а не только время на клиенте.
  5. Действие. Сделайте status/current обязательным перед защищённым side effect, выберите понятный logout scope и проверяйте clear cookie на совпадение с issue policy.
  6. Откат. При проблеме UX не возвращайте старый ID в active. Сначала сохраните reject stale ID, затем примените отдельный совместимый rollback для экрана, маршрута или новой политики cookie.

Ограничения и безопасный результат расследования

Fixture не хранит секреты, не реализует random ID/TLS/часы и не знает browser API, пользователей, устройств, cache, CSRF или конкурентности. Она не доказывает stale logout в продукте и не выбирает HTTP-ответ. Она фиксирует лишь правило logout-current: старый ID не оживляет и не отменяет successor. Другой scope требует другой fixture.

Следующий шаг — добавить audit contract: корреляция без bearer secret, server status до protected effect, logout scope и причина reject. Затем проводить отдельный repro только в разрешённой среде. Несобранный факт остаётся неизвестным; это безопаснее правдоподобной, но неповторяемой истории про вкладки.

Историческая граница февраля 2023

Этот маршрут опирается на документы, которые существовали к февралю 2023: RFC 6265 от 2011 года, NIST SP 800-63B от 2017 года с обновлениями до 2020 и draft-ietf-httpbis-rfc6265bis-11 от ноября 2022. Draft не объявляется финальной спецификацией. Ни один из источников не даёт trace конкретного браузера и не заменяет целевую проверку реального logout flow.

Проверяемые источники