Spring Security: Маскировка CSRF-токена

В своих проектах я крайне редко использую защиту от CSRF-атак, предоставляемую Spring Security, ввиду того, что подавляющая часть этих проектов — REST-сервисы без хранения состояния. Но один из проектов использует файлы cookies на стороне браузера для хранения состояния, а в этом случае для защиты от CSRF-атак нужно использовать средства Spring Security. И при переводе проекта на Spring Boot 3 я обнаружил, что Spring Security начал игнорировать передаваемый в заголовке CSRF-токен.

Причиной такого поведения, как выяснилось, стало использование по умолчанию XorCsrfTokenRequestAttributeHandler вместо CsrfTokenRequestAttributeHandler в Spring Security 6. В качестве быстрого решения я указал использование CsrfTokenRequestAttributeHandler в настройках CSRF, что решило проблему, но я захотел разобраться в ситуации более подробно, и понять как можно использовать XorCsrfTokenRequestAttributeHandler.

BREACH-атака

Причиной таких изменений в валидации CSRF-токенов стала защита от BREACH-атак. Если вкратце, то BREACH-атака — это подвид CRIME-атаки, целью которой является механизмы сжатия HTTP такие, как gzip и DEFLATE. Дело в том, что TLS 1.2 и его более ранние версии могут шифровать сжатые данные без корректной обфускации размера незашифрованных данных, что делает возможным получение HTTP-заголовков при MitM-атаках.

Как несложно догадаться, при такой уязвимости значение заголовка Cookie может утечь достаточно быстро, так как меняется редко. То же самое можно сказать и про постоянный или редко изменяющийся CSRF-токен, а получение сессионной cookie и CSRF-токена — это уже полноценный угон пользовательской сессии.

Собственно, для защиты от BREACH-атак в Spring Security и была введена маскировка CSRF-токена.

Маскировка CSRF-токена

Основной проблемой при использовании XorCsrfTokenRequestAttributeHandler, на мой взгляд, является банальное отсутствие документации, из-за чего приходится читать исходный код, чтобы понять, как работает маскировка CSRF-токена в данном случае.

В качестве исходного CSRF-токена всё также используется UUID. Замаскированный CSRF-токен представляет собой строку Base64, состоящую из случайного набора байт, размер которого может быть больше или равен размеру исходного CSRF-токена (36 байт), и результата выполнения строгой дизъюнкции (XOR) между байтами случайного набора и исходного CSRF-токена. При этом если размер случайного набора байт больше CSRF-токена, то для строгой дизъюнкции будут использованы только первые 36 байт из набора.

Всё вышесказанное можно описать следующими строками кода:

Результат выполнения будет примерно следующим:

От себя могу лишь добавить, что если вы намерены использовать XorCsrfTokenRequestAttributeHandler, то для каждого запроса, в котором будет передаваться CSRF-токен, вы должны маскировать его заново, чтобы не использовалось повторяющееся значение, иначе все старания будут напрасными. Так же могу посоветовать использовать маску размером больше CSRF-токена и не делать этот размер фиксированным, чтобы маскированный CSRF-токен не был одного размера.

Полезные ссылки