Flask, WTForms и AJAX: CSRF защита, before_request и многоязычие
Вы всегда должны проверять, работает ли защита CSRF . С Flask это не очевидно.
Я никогда не проверял, работает ли защита CSRF в моем приложении Flask , на этом сайте. Включено ли оно по умолчанию? Из документации по расширению Flask_WTF:
Любое представление, использующее FlaskForm для обработки запроса, уже получает защиту CSRF .
И из текста сообщения Miguel Grinberg 'Cookie Security for Flask Applications':
Если вы работаете с вашими веб-формами с расширением Flask-WTF , то по умолчанию вы уже защищены от CSRF на ваших формах.
Она должна быть включена, давайте проверим, правда ли это. На странице с формой, которую я добавил:
<script>
$(document).ready(function(){
$('#csrf_token').val('ABC');
});
</script>
Потом я обновил форму. В отладчике я проверил, что csrf_token изменился на 'ABC'. Нажатие кнопки submit должно дать ошибку CSRF , ну я ожидал исключение CSRF . Для меня нет исключения, что означает отсутствие защиты CSRF . Как такое возможно? Это связано с тем, что я использую диспетчерское программное обеспечение DispatcherMiddleWare или что-то еще не так?
Да, что-то было не так. Я! И это было вызвано TL;DR. Фактически произошла ошибка CSRF , но не исключение CSRF . У меня много форм, в основном в разделе администрирования, и они постоянно работают с защитой CSRF без ошибок CSRF . Что происходит?
Мой тестапп:
def create_app():
fname = 'create_app'
print(fname + '()')
app = Flask(__name__)
app.config['SECRET_KEY'] = 'some-secret-key'
class MyCSRFForm(FlaskForm):
submit = SubmitField(_l('Send'))
@app.route('/csrf_form', methods=['GET', 'POST'])
def csrf_form():
fname = 'csrf_form'
print(fname + '()')
form = MyCSRFForm()
if form.validate_on_submit():
print(fname + ': no validate_on_submit errors, processing form')
print(fname + ': form.errors = '.format(form.errors))
for field, errors in form.errors.items():
print(fname + ': form.errors, field = {}, errors = {}'.format(field, errors))
return render_template('csrf_form.html', form=form)
и шаблон csrf_form.html:
<html>
<head></head>
<body>
<form method="post">
{{ form.csrf_token }}
<p>{{ form.submit() }}</p>
</form>
<script>
elem = document.getElementById('csrf_token');
elem.setAttribute('value', 'ABC');
</script>
</body>
</html>
Результат:
csrf_form: form.errors, field = csrf_token, errors = ['The CSRF token is invalid.']
Более подробная информация об этом в разделе 'Несоответствие с повышением CSRFError #381', смотрите ссылки ниже. Похоже, что вы можете использовать защиту Flask-WTF CSRF двумя способами:
- в качестве сообщения об ошибке CSRF во время проверки формы.
- как исключение CSRF
И то, и другое имеет свои преимущества и недостатки. Я решил пойти на исключение CSRF , потому что случайно забыть об этом нельзя. Для генерации исключений CSRF я добавил предложенный код в свою __init__.py:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect()
...
def create_app(project_config):
...
# csrf protection
csrf.init_app(app)
Теперь, когда я отправляю форму, я получаю исключение CSRF :
Bad Request
The CSRF token is invalid.
Я изменил jQuery и удалил имя поля csrf_token:
<script>
$(document).ready(function(){
$('#csrf_token').attr({name: 'nothing'});
});
</script>
Обновить и отправить, исключение CSRF :
Bad Request
The CSRF token is missing.
И в качестве финального теста мы удаляем сценарий и устанавливаем время истечения маркера CSRF в create_app() равным 5 секундам:
app.config['WTF_CSRF_TIME_LIMIT'] = 5
Исключение CSRF :
Bad Request
The CSRF token has expired.
Резюме этих тестов:
- Код ошибки http - 400 Плохой запрос.
- Получилось, что защита CSRF работает по умолчанию и может быть реализована двумя способами, в документации на это нет достаточной ясности.
- Мне нужны были исключения CSRF и я добавил предложенный дополнительный код.
- Для условий было очень просто сгенерировать исключения CSRF :
- Микросхема CSRF недействительна
- Микросхема CSRF отсутствует
- Срок действия микросхемы CSRF истек
Несколько форм и маркер CSRF
Страница записи блога на этом сайте имеет два типа comment form: comment form и comment reply form, см. также предыдущую запись.
Глядя на страницу с comment form и comment reply form , я заметил, что после загрузки страницы у обоих было значение CSRF .
comment form:
<input id="comment_form-csrf_token" name="comment_form-csrf_token" type="hidden" value="IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6ReA.dFbMlM0bClY6BvAVjxZ0GfiFBM4">
comment reply form:
<input id="comment_reply_form-csrf_token" name="comment_reply_form-csrf_token" type="hidden" value="IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6ReA.dFbMlM0bClY6BvAVjxZ0GfiFBM4">
или..:
comment_form-csrf_token: IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6ReA.dFbMlM0bClY6BvAVjxZ0GfiFBM4
comment_reply_form-csrf_token: IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6ReA.dFbMlM0bClY6BvAVjxZ0GfiFBM4
Затем я отправляю слишком короткое сообщение, используя comment reply form. Форма посылается на сервер, выдается на сервере и отправляется обратно клиенту.
Проверка показала, что маркер CSRF comment reply form изменился:
comment_reply_form-csrf_token: IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6SUg.6axMGCN2KmQQ4WeAAxYgTg6UdAs
Здесь возникли следующие вопросы:
- Как построена маркер CSRF ?
- Когда истекает срок действия маркера CSRF ?
- Требуются ли разные формы на странице разные токены CSRF ?
- Как может измениться маркер CSRF comment reply form ?
- Что такое X-CSRFToken?
Как построена маркер CSRF ?
Пора снова начать читать. Из руководства OWASP :
В общем, разработчикам достаточно сгенерировать этот токен только один раз за текущую сессию. После первоначальной генерации данной токена, значение сохраняется в сессии и используется для каждого последующего запроса до окончания сессии.
В Flask-WTF сырая лексема находится в сессии, а подписанная лексема - в g. Или, точнее говоря, сырой токен всегда находится в сессии, а подписанный токен находится в g после использования формы. Перед тем, как инстанцировать форму:
session['csrf_token']: 387f76a30ad4c09663398b455c8d04272672d18b
g.csrf_token: None
После инстанцирования формы:
session['csrf_token']: 387f76a30ad4c09663398b455c8d04272672d18b
g.csrf_token: IjM4N2Y3NmEzMGFkNGMwOTY2MzM5OGI0NTVjOGQwNDI3MjY3MmQxOGIi.Xk6vnw.L7uNOptXpBxemh_TIEy3e9Ujllg
Подписанный токен помещается в g таким образом, чтобы при последующих запросах, например, в случае нескольких форм, можно было использовать один и тот же подписанный токен, и его не нужно было бы генерировать заново. При желании вы можете поэкспериментировать с генерацией и проверкой лексем, используя функции generate_csrf() и validate_csrf():
from flask_wtf.csrf import generate_csrf, validate_csrf
secret_key = current_app.config['SECRET_KEY']
# default: WTF_CSRF_FIELD_NAME = 'csrf_token'
token_key = current_app.config['WTF_CSRF_FIELD_NAME']
csrf_token = generate_csrf(secret_key=secret_key, token_key=token_key)
current_app.logger.debug(fname + ': csrf_token = {}'.format(csrf_token))
validated = False
try:
validate_csrf(csrf_token, secret_key=secret_key, time_limit=3600, token_key=token_key)
validated = True
except:
pass
current_app.logger.debug(fname + ': validated = {}'.format(validated))
Вышеуказанные функции делают что-то вроде черного ящика. Но мы видим, что сгенерированный токен подписан и включает в себя метку времени.
Когда истекает срок действия маркера CSRF ?
Мы уже видели, что временем истечения срока действия токена CSRF можно управлять:
app.config['WTF_CSRF_TIME_LIMIT'] = 5
По умолчанию 3600, один час. На этот раз где-то в жетоне. Существует два возможных способа реализации этого:
- Время старта находится в маркере CSRF
- Время окончания находится в маркере CSRF
Это не важно знать, если вы не хотите изменить время истечения срока действия в вашем заявлении. Функция generate_csrf() не включает параметр time_limit, а validate_csrf включает. Это будет означать, что время старта находится в маркере CSRF . Для проверки этого я поместил на экран страницу с формой, затем изменил стандартное значение time_limit на 10 секунд и затем отправил форму. Как и ожидалось, было поднято исключение CSRF .
Есть еще одно условие, когда истекает срок действия вашего токена CSRF , а именно, когда истекает сессия. Причина в том, что сырая лексема CSRF хранится в сеансе. Это означает, что если ваша сессия проработает короче, чем время истечения срока действия вашего маркера CSRF , вы можете получить ошибки CSRF .
Требуются ли разные формы на странице разные токены CSRF ?
Отвечай: Это более общий вопрос. Ответ - нет. Мы можем использовать одну и ту же маркер CSRF для всех форм на странице.
Как может измениться маркер CSRF comment reply form ?
Отвечай: Токен не меняется, но подписанный токен меняется, потому что в подписанном токене также есть временная метка. Если вы посмотрите на подписанные маркеры выше, вы заметите, что первая часть идентична.
Параметр заголовка X-CSRFToken
Возможно, Вы читали о маркере X-CSRFToken. X-CSRFToken является параметром в заголовке. Обычно нам это не нужно. Если у вас есть параметр CSRF в вашем AJAX POST , то он будет использоваться. Если нет, то можно установить это в заголовке POST , используя jQuery:
var csrf_token = ...
...
$.ajax({
beforeSend: function(xhr, settings){
if(!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain){
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
},
Это зависит от Вашей реализации, если Вы посылаете формы с параметром CSRF маркера или если Вы устанавливаете параметр заголовка X-CSRFToken. В документации сказано, что вы можете использовать это только в режиме исключения 'CSRF ' (добавление дополнительного кода). Я не проверял это.
Обработка исключений CSRF и before_request
Я уже работаю с исключениями ошибок HTTP, такими как 401 (Unauthorized), 403 (Forbidden), 404 (Not Found), 405 (Method Not Allowed), 500 (Internal Server Error), но как мы работаем с исключением CSRF ? Ошибка CSRF распространяется на 400 (Плохой запрос).
Я создал красивые бубенцы и страницы со свистками, показывающие исключения. Я сделал это, полагаясь на то, что был вызван before_request .
В before_request I обрабатывает входящий запрос и, кроме всего прочего, соответствующим образом устанавливает выбранный язык.
К сожалению, по умолчанию before_request НЕ вызывается с исключением CSRF . Но есть способ обойти это. Мы можем установить:
app.config['WTF_CSRF_CHECK_DEFAULT'] = False
В результате будет вызван before_request . Затем в before_request, после некоторой существенной обработки, мы можем вызвать:
csrf.protect()
Это вызовет CSFR-исключение, если оно есть, и соответственно вызовет обработчик ошибок.
Многоязычные сообщения исключений CSRF
Я использую Flask-Babel и сообщения о проверке переведены на выбранный язык. Сообщения исключений CSRF не переводили и проверяли csrf.py оказалось, что эти сообщения жестко закодированы в English(?). На данный момент я создал словарь для работы с переводом:
csrf_error_message2translated_error_messages = {
...
'The CSRF token has expired.': _('The CSRF token has expired.'),
...
}
if error.description in error_description2error_messages:
error_message = error_description2error_messages[error.description]
else:
error_message = error.description
Реагирование на исключения, в том числе на запросы CSRF, на запросы AJAX
С AJAX мы не можем показать нашу милую страницу ошибок HTML в виде исключения. По запросу мы должны вернуть код ошибки или HTML , который понимает клиент. Это сводится к возвращению клиенту исключения в виде сообщения об ошибке в кодировке JSON . В Flask у нас уже есть обработчики ошибок.
Но откуда нам знать, что запрос приходил с вызова AJAX ? Я отправляю форму с кодировкой application/x-www-form-urlencoded:
$.ajax({
data: $('#' + form_id).serialize() + '&' + encodeURI( $(submit_button).attr('name') ) + '=' + encodeURI( $(submit_button).val() ),
type: 'POST',
url: url_comment_new,
contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
dataType: 'json'
})
Один из способов - посмотреть на полученный заголовок "Принять".
current_app.logger.error(fname + ': request.accept_mimetypes = {}'.format(request.accept_mimetypes))
# text/javascript,application/json,*/*;q=0.01
Из документа 'Список принятых по умолчанию значений':
Обратите внимание, что все браузеры добавляют */* MIME тип, чтобы охватить все случаи. Обычно это используется для запросов, инициируемых через адресную строку браузера, или через элемент HTML <a>.
Это означает, что 'request.accept_mimetypes.accept_json' и 'request.accept_mimetypes.accept_html' являются True. Не очень полезно. Типы accept_mimetypes перечислены в порядке предпочтений. Мы могли просканировать список и решить вернуть JSON при встрече с 'application/json' до 'text/html'. Но я не знаю точно, какие браузеры посылают, которые accept_mimetypes, а это значит, что в данный момент я не на 100% уверен, что страницы ошибки 'without AJAX' не будут случайно показаны как JSON.
Гораздо более безопасным способом является добавление параметра в запросы AJAX и проверка в обработчике ошибок наличия этого параметра. Если она присутствует, то мы возвращаем сообщение об ошибке JSON , если нет, то выводим страницу стандартной ошибки. Мне это не нравится, но это работает, и какое-то время будет копать глубже в этом.
Резюме
Вы всегда должны проверять, работает ли защита CSRF . Я этого не делал, и это было началом большого чтения. Оказалось, что это два способа справиться с защитой CSRF , ошибка формы CSRF и исключение CSRF . Я выбираю вторую. Для меня работа с защитой CSRF для этого мультиязычного сайта не была стандартной, основной причиной было то, что before_request не вызывается по умолчанию на исключении CSRF . В before_request я проверяю и устанавливаю язык среди прочего, чтобы он действительно работал. К счастью, мы можем задержать защиту CSRF и явно вызвать ее в before_request после существенной обработки с использованием csrf.protect(). У нас уже есть страницы ошибок исключения, но с запросами AJAX мы должны вернуть сообщения об ошибках JSON . Определение того, поступает ли запрос от AJAX , является сложным, поэтому я использовал дополнительный параметр в запросах AJAX .
Ссылки / кредиты
Cookie Security for Flask Applications
https://blog.miguelgrinberg.com/post/cookie-security-for-flask-applications
Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet
https://owasp.org/www-project-cheat-sheets/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#Prevention_Measures_That_Do_NOT_Work
Flask-WTF CSRF Protection
https://flask-wtf.readthedocs.io/en/latest/csrf.html
Generating CSRF tokens for multiple forms on a single page
https://stackoverflow.com/questions/14715250/generating-csrf-tokens-for-multiple-forms-on-a-single-page
How to use Flask-WTForms CSRF protection with AJAX?
https://stackoverflow.com/questions/31888316/how-to-use-flask-wtforms-csrf-protection-with-ajax
Inconsistency with raising CSRFError #381
https://github.com/lepture/flask-wtf/issues/381
List of default Accept values
https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values
Posting a WTForm via AJAX with Flask
https://medium.com/@doobeh/posting-a-wtform-via-ajax-with-flask-b977782edeee
Unacceptable Browser HTTP Accept Headers (Yes, You Safari and Internet Explorer)
https://www.newmediacampaigns.com/blog/browser-rest-http-accept-headers
Подробнее
CSRF protection Flask
Оставить комментарий
Комментируйте анонимно или войдите в систему, чтобы прокомментировать.
Комментарии (1)
Оставьте ответ
Ответьте анонимно или войдите в систему, чтобы ответить.
Thanks for placing this post.
I have a problem with my webapp that raises CSRF errors, which I asked in
https://stackoverflow.com/questions/65985400/getting-csrf-errors-from-put-fetch-request-that-does-not-involve-flask-forms
Can you please explain why I'm getting CSRF error in the first place, and how to fix the problem?
Thanks again.
Avner
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу