Update LICENSE, README.md, setup.py, quart_csrf/csrf.py, quart_csrf/__init__.py files

This commit is contained in:
Wagner Corrales 2020-11-10 02:28:58 +00:00
parent 747a3d67af
commit c9d00596df
5 changed files with 382 additions and 0 deletions

21
LICENSE Normal file
View 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
View 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
View File

@ -0,0 +1 @@
from .csrf import generate_csrf, validate_csrf, CSRFProtect

253
quart_csrf/csrf.py Normal file
View 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
View 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',
)