angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Flask, WTForms и AJAX: CSRF защита, before_request и многоязычие

Вы всегда должны проверять, работает ли защита CSRF . С Flask это не очевидно.

29 февраля 2020
post main image
https://unsplash.com/@christineashleydonaldson

Я никогда не проверял, работает ли защита 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)

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.

avatar

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