Update LICENSE, README.md, setup.py, quart_csrf/csrf.py, quart_csrf/__init__.py files
This commit is contained in:
parent
747a3d67af
commit
c9d00596df
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 wagner
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
75
README.md
Normal file
75
README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
Quart-Csrf
|
||||||
|
==========
|
||||||
|
|
||||||
|
Quart-Csrf is an extension for `Quart
|
||||||
|
<https://gitlab.com/pgjones/quart>`_ to provide CSRF protection.
|
||||||
|
The code is taked from `Flask-WTF
|
||||||
|
<https://github.com/lepture/flask-wtf>`_
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
To enable CSRF protection globally for a Quart app, you have to create an CSRFProtect and
|
||||||
|
initialise it with the application,
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from quart_csrf import CSRFProtect
|
||||||
|
|
||||||
|
app = Quart(__name__)
|
||||||
|
CSRFProtect(app)
|
||||||
|
|
||||||
|
or via the factory pattern,
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
csrf = CSRFProtect()
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
app = Quart(__name__)
|
||||||
|
csrf.init_app(app)
|
||||||
|
return app
|
||||||
|
|
||||||
|
Note: CSRF protection requires a secret key to securely sign the token. By default this will
|
||||||
|
use the QUART app's SECRET_KEY. If you'd like to use a separate token you can set QUART_CSRF_SECRET_KEY.
|
||||||
|
|
||||||
|
HTML Forms: render a hidden input with the token in the form.
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
JavaScript Requests: When sending an AJAX request, add the X-CSRFToken header to it. For example, in jQuery you can configure all requests to send the token.
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var csrf_token = $('meta[name=csrf-token]').attr('content'); // "{{ csrf_token() }}";
|
||||||
|
|
||||||
|
$.ajaxSetup({
|
||||||
|
beforeSend: function(xhr, settings) {
|
||||||
|
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
|
||||||
|
xhr.setRequestHeader("X-CSRFToken", csrf_token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
Quart-Csrf is developed on `GitLab
|
||||||
|
<https://gitlab.com/wcorrales/quart-csrf>`_. You are very welcome to
|
||||||
|
open `issues <https://gitlab.com/wcorrales/quart-csrf/issues>`_ or
|
||||||
|
propose `merge requests
|
||||||
|
<https://gitlab.com/wcorrales/quart-csrf/merge_requests>`_.
|
||||||
|
|
||||||
|
Help
|
||||||
|
----
|
||||||
|
|
||||||
|
This README is the best place to start, after that try opening an
|
||||||
|
`issue <https://gitlab.com/wcorrales/quart-csrf/issues>`_.
|
1
quart_csrf/__init__.py
Normal file
1
quart_csrf/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .csrf import generate_csrf, validate_csrf, CSRFProtect
|
253
quart_csrf/csrf.py
Normal file
253
quart_csrf/csrf.py
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from itsdangerous import BadData, SignatureExpired, URLSafeTimedSerializer
|
||||||
|
from quart import current_app, g, request, session
|
||||||
|
from werkzeug.exceptions import BadRequest
|
||||||
|
from werkzeug.security import safe_str_cmp
|
||||||
|
from wtforms import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ('generate_csrf', 'validate_csrf', 'CSRFProtect')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_csrf(secret_key=None, token_key=None):
|
||||||
|
"""Generate a CSRF token. The token is cached for a request, so multiple
|
||||||
|
calls to this function will generate the same token.
|
||||||
|
|
||||||
|
During testing, it might be useful to access the signed token in
|
||||||
|
``g.csrf_token`` and the raw token in ``session['csrf_token']``.
|
||||||
|
|
||||||
|
:param secret_key: Used to securely sign the token. Default is
|
||||||
|
``QUART_CSRF_SECRET_KEY`` or ``SECRET_KEY``.
|
||||||
|
:param token_key: Key where token is stored in session for comparison.
|
||||||
|
Default is ``QUART_CSRF_FIELD_NAME`` or ``'csrf_token'``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
secret_key = _get_config(
|
||||||
|
secret_key, 'QUART_CSRF_SECRET_KEY', current_app.secret_key,
|
||||||
|
message='A secret key is required to use CSRF.'
|
||||||
|
)
|
||||||
|
field_name = _get_config(
|
||||||
|
token_key, 'QUART_CSRF_FIELD_NAME', 'csrf_token',
|
||||||
|
message='A field name is required to use CSRF.'
|
||||||
|
)
|
||||||
|
|
||||||
|
if field_name not in g:
|
||||||
|
s = URLSafeTimedSerializer(secret_key, salt='quart-csrf-token')
|
||||||
|
|
||||||
|
if field_name not in session:
|
||||||
|
session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest()
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = s.dumps(session[field_name])
|
||||||
|
except TypeError:
|
||||||
|
session[field_name] = hashlib.sha1(os.urandom(64)).hexdigest()
|
||||||
|
token = s.dumps(session[field_name])
|
||||||
|
|
||||||
|
setattr(g, field_name, token)
|
||||||
|
|
||||||
|
return g.get(field_name)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_csrf(data, secret_key=None, time_limit=None, token_key=None):
|
||||||
|
"""Check if the given data is a valid CSRF token. This compares the given
|
||||||
|
signed token to the one stored in the session.
|
||||||
|
|
||||||
|
:param data: The signed CSRF token to be checked.
|
||||||
|
:param secret_key: Used to securely sign the token. Default is
|
||||||
|
``QUART_CSRF_SECRET_KEY`` or ``SECRET_KEY``.
|
||||||
|
:param time_limit: Number of seconds that the token is valid. Default is
|
||||||
|
``QUART_CSRF_TIME_LIMIT`` or 3600 seconds (60 minutes).
|
||||||
|
:param token_key: Key where token is stored in session for comparison.
|
||||||
|
Default is ``QUART_CSRF_FIELD_NAME`` or ``'csrf_token'``.
|
||||||
|
|
||||||
|
:raises ValidationError: Contains the reason that validation failed.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.14
|
||||||
|
Raises ``ValidationError`` with a specific error message rather than
|
||||||
|
returning ``True`` or ``False``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
secret_key = _get_config(
|
||||||
|
secret_key, 'QUART_CSRF_SECRET_KEY', current_app.secret_key,
|
||||||
|
message='A secret key is required to use CSRF.'
|
||||||
|
)
|
||||||
|
field_name = _get_config(
|
||||||
|
token_key, 'QUART_CSRF_FIELD_NAME', 'csrf_token',
|
||||||
|
message='A field name is required to use CSRF.'
|
||||||
|
)
|
||||||
|
time_limit = _get_config(
|
||||||
|
time_limit, 'QUART_CSRF_TIME_LIMIT', 3600, required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
raise ValidationError('The CSRF token is missing.')
|
||||||
|
|
||||||
|
if field_name not in session:
|
||||||
|
raise ValidationError('The CSRF session token is missing.')
|
||||||
|
|
||||||
|
s = URLSafeTimedSerializer(secret_key, salt='quart-csrf-token')
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = s.loads(data, max_age=time_limit)
|
||||||
|
except SignatureExpired:
|
||||||
|
raise ValidationError('The CSRF token has expired.')
|
||||||
|
except BadData:
|
||||||
|
raise ValidationError('The CSRF token is invalid.')
|
||||||
|
|
||||||
|
if not safe_str_cmp(session[field_name], token):
|
||||||
|
raise ValidationError('The CSRF tokens do not match.')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_config(
|
||||||
|
value, config_name, default=None,
|
||||||
|
required=True, message='CSRF is not configured.'
|
||||||
|
):
|
||||||
|
"""Find config value based on provided value, Flask config, and default
|
||||||
|
value.
|
||||||
|
|
||||||
|
:param value: already provided config value
|
||||||
|
:param config_name: Flask ``config`` key
|
||||||
|
:param default: default value if not provided or configured
|
||||||
|
:param required: whether the value must not be ``None``
|
||||||
|
:param message: error message if required config is not found
|
||||||
|
:raises KeyError: if required config is not found
|
||||||
|
"""
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
value = current_app.config.get(config_name, default)
|
||||||
|
|
||||||
|
if required and value is None:
|
||||||
|
raise RuntimeError(message)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class CSRFProtect:
|
||||||
|
"""Enable CSRF protection globally for a Flask app.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
|
Checks the ``csrf_token`` field sent with forms, or the ``X-CSRFToken``
|
||||||
|
header sent with JavaScript requests. Render the token in templates using
|
||||||
|
``{{ csrf_token() }}``.
|
||||||
|
|
||||||
|
See the :ref:`csrf` documentation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, app=None):
|
||||||
|
if app:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
app.extensions['csrf'] = self
|
||||||
|
|
||||||
|
app.config.setdefault('QUART_CSRF_ENABLED', True)
|
||||||
|
app.config.setdefault('QUART_CSRF_CHECK_DEFAULT', True)
|
||||||
|
app.config['QUART_CSRF_METHODS'] = set(app.config.get(
|
||||||
|
'QUART_CSRF_METHODS', ['POST', 'PUT', 'PATCH', 'DELETE']
|
||||||
|
))
|
||||||
|
app.config.setdefault('QUART_CSRF_FIELD_NAME', 'csrf_token')
|
||||||
|
app.config.setdefault(
|
||||||
|
'QUART_CSRF_HEADERS', ['X-CSRFToken', 'X-CSRF-Token']
|
||||||
|
)
|
||||||
|
app.config.setdefault('QUART_CSRF_TIME_LIMIT', 3600)
|
||||||
|
app.config.setdefault('QUART_CSRF_SSL_STRICT', True)
|
||||||
|
|
||||||
|
app.jinja_env.globals['csrf_token'] = generate_csrf
|
||||||
|
app.context_processor(lambda: {'csrf_token': generate_csrf})
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
async def csrf_protect():
|
||||||
|
if not app.config['QUART_CSRF_ENABLED']:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not app.config['QUART_CSRF_CHECK_DEFAULT']:
|
||||||
|
return
|
||||||
|
|
||||||
|
if request.method not in app.config['QUART_CSRF_METHODS']:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not request.endpoint:
|
||||||
|
return
|
||||||
|
|
||||||
|
await self.protect()
|
||||||
|
|
||||||
|
async def _get_csrf_token(self):
|
||||||
|
# find the token in the form data
|
||||||
|
form = await request.form
|
||||||
|
field_name = current_app.config['QUART_CSRF_FIELD_NAME']
|
||||||
|
base_token = form.get(field_name)
|
||||||
|
|
||||||
|
if base_token:
|
||||||
|
return base_token
|
||||||
|
|
||||||
|
# if the form has a prefix, the name will be {prefix}-csrf_token
|
||||||
|
for key in form:
|
||||||
|
if key.endswith(field_name):
|
||||||
|
csrf_token = form[key]
|
||||||
|
|
||||||
|
if csrf_token:
|
||||||
|
return csrf_token
|
||||||
|
|
||||||
|
# find the token in the headers
|
||||||
|
for header_name in current_app.config['QUART_CSRF_HEADERS']:
|
||||||
|
csrf_token = request.headers.get(header_name)
|
||||||
|
|
||||||
|
if csrf_token:
|
||||||
|
return csrf_token
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def protect(self):
|
||||||
|
if request.method not in current_app.config['QUART_CSRF_METHODS']:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_csrf(await self._get_csrf_token())
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.info(e.args[0])
|
||||||
|
self._error_response(e.args[0])
|
||||||
|
|
||||||
|
if request.is_secure and current_app.config['QUART_CSRF_SSL_STRICT']:
|
||||||
|
if not request.referrer:
|
||||||
|
self._error_response('The referrer header is missing.')
|
||||||
|
|
||||||
|
good_referrer = f'https://{request.host}/'
|
||||||
|
|
||||||
|
if not same_origin(request.referrer, good_referrer):
|
||||||
|
self._error_response('The referrer does not match the host.')
|
||||||
|
|
||||||
|
g.csrf_valid = True # mark this request as CSRF valid
|
||||||
|
|
||||||
|
def _error_response(self, reason):
|
||||||
|
raise CSRFError(reason)
|
||||||
|
|
||||||
|
|
||||||
|
class CSRFError(BadRequest):
|
||||||
|
"""Raise if the client sends invalid CSRF data with the request.
|
||||||
|
|
||||||
|
Generates a 400 Bad Request response with the failure reason by default.
|
||||||
|
Customize the response by registering a handler with
|
||||||
|
:meth:`flask.Flask.errorhandler`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
description = 'CSRF validation failed.'
|
||||||
|
|
||||||
|
|
||||||
|
def same_origin(current_uri, compare_uri):
|
||||||
|
current = urlparse(current_uri)
|
||||||
|
compare = urlparse(compare_uri)
|
||||||
|
|
||||||
|
return (
|
||||||
|
current.scheme == compare.scheme
|
||||||
|
and current.hostname == compare.hostname
|
||||||
|
and current.port == compare.port
|
||||||
|
)
|
32
setup.py
Normal file
32
setup.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
with open("README.md", "r") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='quart-csrf',
|
||||||
|
version='0.1',
|
||||||
|
author='Wagner Corrales',
|
||||||
|
author_email='wagnerc4@gmail.com',
|
||||||
|
description='Quart CSRF Protection',
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
|
url='https://gitlab.com/wcorrales/quart-csrf',
|
||||||
|
packages=['quart_csrf'],
|
||||||
|
install_requires=['itsdangerous', 'quart', 'wtforms'],
|
||||||
|
license='MIT',
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 1 - Alpha',
|
||||||
|
'Environment :: Web Environment',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: MIT License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
|
],
|
||||||
|
python_requires='>=3.7',
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user