Integrated my history into data
This commit is contained in:
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/Users/karthikpullela/Desktop/hackdavis/Flex/flex/bin/python3
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import sys
|
||||
|
||||
from jwt.__main__ import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit(main())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'social_django',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -63,11 +64,20 @@ TEMPLATES = [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'social_django.context_processors.backends',
|
||||
'social_django.context_processors.login_redirect',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'social_core.backends.open_id.OpenIdAuth',
|
||||
'social_core.backends.google.GoogleOpenId',
|
||||
'social_core.backends.google.GoogleOAuth2',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
WSGI_APPLICATION = 'flex.wsgi.application'
|
||||
|
||||
|
||||
@@ -119,4 +129,8 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
LOGIN_URL = 'log'
|
||||
LOGIN_REDIRECT_URL ='/home/dash'
|
||||
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY ='603222874022-4msscrpqkgc4o8qn9ng3r0tetadcl69t.apps.googleusercontent.com' #Paste CLient Key
|
||||
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'NgVaidrQ8rG5uMhMKfPOv9QI' #Paste Secret Key
|
||||
|
||||
@@ -21,5 +21,6 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('home/', include('home.urls')),
|
||||
path('logout/', auth_views.logout, name='logout'),
|
||||
path('accounts/', include('django.contrib.auth.urls'))
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('auth/', include('social_django.urls', namespace='social')),
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,11 @@
|
||||
/* "#00897B" */
|
||||
/* subtitle */
|
||||
/* button */
|
||||
|
||||
/* 603222874022-4msscrpqkgc4o8qn9ng3r0tetadcl69t.apps.googleusercontent.com
|
||||
*/
|
||||
|
||||
/* NgVaidrQ8rG5uMhMKfPOv9QI */
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -164,6 +169,12 @@ button:hover {
|
||||
box-shadow: inset 0 0 10px #000000;
|
||||
cursor: pointer;
|
||||
}
|
||||
#googol{
|
||||
position: relative;
|
||||
left: 45%;
|
||||
top: 90%;
|
||||
color: white;
|
||||
}
|
||||
#begin{
|
||||
color: white;
|
||||
font-size: 25px;
|
||||
|
||||
@@ -17,18 +17,29 @@
|
||||
<script src="https://use.fontawesome.com/7b29adddad.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
var i = 5;
|
||||
a = [];
|
||||
b = [];
|
||||
{% for num in walk_data %}
|
||||
a.push(parseInt({{num}}));
|
||||
{% endfor %}
|
||||
|
||||
{% for fit in calories_data %}
|
||||
b.push(parseInt({{fit}}));
|
||||
{% endfor %}
|
||||
|
||||
google.charts.load('current', {'packages':['corechart']});
|
||||
google.charts.setOnLoadCallback(drawChart);
|
||||
function drawChart() {
|
||||
var data = google.visualization.arrayToDataTable([
|
||||
['Task', 'Hours per Day'],
|
||||
['Completed', 11],
|
||||
['To Go' , 13],
|
||||
['Completed', 10],
|
||||
['To Go' , 2],
|
||||
]);
|
||||
var options = {
|
||||
title: 'Personal Goal Graph',
|
||||
pieHole: 0.8,
|
||||
colors:['red','#004411'],
|
||||
pieHole: 0.7,
|
||||
colors:['orange','white'],
|
||||
};
|
||||
var chart = new google.visualization.PieChart(document.getElementById('piechart'));
|
||||
chart.draw(data, options);
|
||||
@@ -41,12 +52,13 @@
|
||||
// Some raw data (not necessarily accurate)
|
||||
var data = google.visualization.arrayToDataTable([
|
||||
['Day', 'Steps', 'Calories', 'Sleep', 'Activity', 'Average'],
|
||||
['01/05', 10, 94, 90, 84, 70],
|
||||
['01/06', 70, 90, 91, 84, 84],
|
||||
['01/07', 96, 98, 70, 93, 90],
|
||||
['01/08', 83, 90, 90, 75, 85],
|
||||
['01/09', 80, 90, 93, 95, 90]
|
||||
['01/05', a[0], b[0], 90, 84, 70],
|
||||
['01/06', a[1], b[1], 91, 84, 84],
|
||||
['01/07', a[2], b[2], 70, 93, 90],
|
||||
['01/08', a[3], b[3], 90, 75, 85],
|
||||
['01/09', a[4], b[4], 93, 95, 90]
|
||||
]);
|
||||
|
||||
var options = {
|
||||
title : 'Daily Fitness Goals',
|
||||
vAxis: {title: 'Percent'},
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
|
||||
<script type="text/javascript" src="jquery-3.1.1.min.js"></script>
|
||||
|
||||
<meta name="google-signin-scope" content="profile email">
|
||||
<meta name="google-signin-client_id" content="603222874022-4msscrpqkgc4o8qn9ng3r0tetadcl69t.apps.googleusercontent.com">
|
||||
<script src="https://apis.google.com/js/platform.js" async defer></script>
|
||||
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Rubik" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Patua+One" rel="stylesheet">
|
||||
@@ -63,7 +67,8 @@
|
||||
<p class="inputs">
|
||||
</p>
|
||||
<br>
|
||||
<a href="{% url 'log' %}"><button id="begin">BEGIN</button></a>
|
||||
<a href="{% url ''social:begin' 'google-oauth2'' %}"><button id="begin">BEGIN</button></a><br>
|
||||
|
||||
<br>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ appname = 'home'
|
||||
urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
path('signin', views.user_login, name='signin'),
|
||||
path('return_data', views.return_data, name='return_data'),
|
||||
path('log', views.log, name='log'),
|
||||
path('dash', views.dash, name='dash'),
|
||||
|
||||
|
||||
@@ -53,6 +53,15 @@ def user_login(request):
|
||||
return HttpResponse("It's not supposed to come till here.")
|
||||
|
||||
|
||||
def return_data(request):
|
||||
if request.user.is_authenticated:
|
||||
uri = 'https://www.googleapis.com/fitness/v1/users/me/dataSources'
|
||||
r = requests.get(uri)
|
||||
return HttpResponse("Works, I guess.", auth=(request.user.username, request.user.password))
|
||||
else:
|
||||
return HttpResponse("User not authenticated")
|
||||
|
||||
|
||||
|
||||
def dash(request):
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
PyJWT
|
||||
=====
|
||||
|
||||
.. image:: https://secure.travis-ci.org/jpadilla/pyjwt.svg?branch=master
|
||||
:target: http://travis-ci.org/jpadilla/pyjwt?branch=master
|
||||
|
||||
.. image:: https://ci.appveyor.com/api/projects/status/h8nt70aqtwhht39t?svg=true
|
||||
:target: https://ci.appveyor.com/project/jpadilla/pyjwt
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/pyjwt.svg
|
||||
:target: https://pypi.python.org/pypi/pyjwt
|
||||
|
||||
.. image:: https://coveralls.io/repos/jpadilla/pyjwt/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/jpadilla/pyjwt?branch=master
|
||||
|
||||
.. image:: https://readthedocs.org/projects/pyjwt/badge/?version=latest
|
||||
:target: https://pyjwt.readthedocs.io
|
||||
|
||||
A Python implementation of `RFC
|
||||
7519 <https://tools.ietf.org/html/rfc7519>`_. Original implementation
|
||||
was written by `@progrium <https://github.com/progrium>`_.
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Install with **pip**:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ pip install PyJWT
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import jwt
|
||||
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
|
||||
|
||||
>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
|
||||
{'some': 'payload'}
|
||||
|
||||
|
||||
Command line
|
||||
------------
|
||||
|
||||
Usage::
|
||||
|
||||
pyjwt [options] INPUT
|
||||
|
||||
Decoding examples::
|
||||
|
||||
pyjwt --key=secret TOKEN
|
||||
pyjwt --no-verify TOKEN
|
||||
|
||||
See more options executing ``pyjwt --help``.
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
View the full docs online at https://pyjwt.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
You can run tests from the project root after cloning with:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ python setup.py test
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,110 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: PyJWT
|
||||
Version: 1.5.3
|
||||
Summary: JSON Web Token implementation in Python
|
||||
Home-page: http://github.com/jpadilla/pyjwt
|
||||
Author: Jose Padilla
|
||||
Author-email: hello@jpadilla.com
|
||||
License: MIT
|
||||
Description-Content-Type: UNKNOWN
|
||||
Keywords: jwt json web token security signing
|
||||
Platform: UNKNOWN
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Topic :: Utilities
|
||||
Provides-Extra: crypto
|
||||
Requires-Dist: cryptography (>=1.4); extra == 'crypto'
|
||||
Provides-Extra: flake8
|
||||
Requires-Dist: flake8; extra == 'flake8'
|
||||
Requires-Dist: flake8-import-order; extra == 'flake8'
|
||||
Requires-Dist: pep8-naming; extra == 'flake8'
|
||||
Provides-Extra: test
|
||||
Requires-Dist: pytest (<4,>3); extra == 'test'
|
||||
Requires-Dist: pytest-cov; extra == 'test'
|
||||
Requires-Dist: pytest-runner; extra == 'test'
|
||||
|
||||
PyJWT
|
||||
=====
|
||||
|
||||
.. image:: https://secure.travis-ci.org/jpadilla/pyjwt.svg?branch=master
|
||||
:target: http://travis-ci.org/jpadilla/pyjwt?branch=master
|
||||
|
||||
.. image:: https://ci.appveyor.com/api/projects/status/h8nt70aqtwhht39t?svg=true
|
||||
:target: https://ci.appveyor.com/project/jpadilla/pyjwt
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/pyjwt.svg
|
||||
:target: https://pypi.python.org/pypi/pyjwt
|
||||
|
||||
.. image:: https://coveralls.io/repos/jpadilla/pyjwt/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/jpadilla/pyjwt?branch=master
|
||||
|
||||
.. image:: https://readthedocs.org/projects/pyjwt/badge/?version=latest
|
||||
:target: https://pyjwt.readthedocs.io
|
||||
|
||||
A Python implementation of `RFC
|
||||
7519 <https://tools.ietf.org/html/rfc7519>`_. Original implementation
|
||||
was written by `@progrium <https://github.com/progrium>`_.
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
Install with **pip**:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ pip install PyJWT
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code:: python
|
||||
|
||||
>>> import jwt
|
||||
>>> encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256')
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg'
|
||||
|
||||
>>> jwt.decode(encoded, 'secret', algorithms=['HS256'])
|
||||
{'some': 'payload'}
|
||||
|
||||
|
||||
Command line
|
||||
------------
|
||||
|
||||
Usage::
|
||||
|
||||
pyjwt [options] INPUT
|
||||
|
||||
Decoding examples::
|
||||
|
||||
pyjwt --key=secret TOKEN
|
||||
pyjwt --no-verify TOKEN
|
||||
|
||||
See more options executing ``pyjwt --help``.
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
View the full docs online at https://pyjwt.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
Tests
|
||||
-----
|
||||
|
||||
You can run tests from the project root after cloning with:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
$ python setup.py test
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
PyJWT-1.5.3.dist-info/DESCRIPTION.rst,sha256=NTi807oEiMl9QPTbA_NX8OLmLAtL4WCN9lj6zmw04bA,1639
|
||||
PyJWT-1.5.3.dist-info/METADATA,sha256=YQMYS9ks5luILyBhUJWEzRxiaowncPD0NfrJDhNgpwE,2911
|
||||
PyJWT-1.5.3.dist-info/RECORD,,
|
||||
PyJWT-1.5.3.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110
|
||||
PyJWT-1.5.3.dist-info/entry_points.txt,sha256=Xl_tLkGbTgywYa7PwaEY2xSiCtVtM2PdHTL4CW_n9dM,45
|
||||
PyJWT-1.5.3.dist-info/metadata.json,sha256=qfgrSQx6qIBvcFkAaFz34q76NkBadsJioyH2k63e9NU,1501
|
||||
PyJWT-1.5.3.dist-info/top_level.txt,sha256=RP5DHNyJbMq2ka0FmfTgoSaQzh7e3r5XuCWCO8a00k8,4
|
||||
jwt/__init__.py,sha256=Au0HJMdNUk9rn1oKHq3qlX4etWu_7UZoGpZJgsb738c,761
|
||||
jwt/__main__.py,sha256=E2cfCobbAPoNorrICDSD4LJTemWTtKT04wsHHp8g2Kk,4151
|
||||
jwt/algorithms.py,sha256=kL1ARjxNL8JeuxEpWS8On14qJWomMX_A_ncIrnZhBrA,13336
|
||||
jwt/api_jws.py,sha256=YYGC3eKhyxeeKZaIZkQdyOVZxz0DwZBK0Ee4iIqBE1M,7555
|
||||
jwt/api_jwt.py,sha256=xWMYu2xSCOAhIpPhO_t-HJ7IDBPFW7LkqJVKOl7cCsw,7068
|
||||
jwt/compat.py,sha256=5cYHQWJuAxcpUQo0e0Fm8-Hn7acYdZQde6M8bnZ6rBg,1784
|
||||
jwt/exceptions.py,sha256=63QgVtqVgRHdVAi0NqRO0s13opKdKZmwUGzW_CyPw4c,841
|
||||
jwt/utils.py,sha256=RraFiloy_xsB8NA1CrlHxS9lR73If8amInQ3P1mKXeM,2629
|
||||
jwt/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
jwt/contrib/algorithms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
jwt/contrib/algorithms/py_ecdsa.py,sha256=tSTUrwx-u14DJcqAChRzJG-wf7bEY2Gv2hI5xSZZNjk,1771
|
||||
jwt/contrib/algorithms/pycrypto.py,sha256=M3nH1Rrk6yb6aPGo6zT4EI_MvPUM4vhO1EwC-uX9JAo,1250
|
||||
../../../bin/pyjwt,sha256=z_4TrA61Dek8m352cf7FY32R6fdqljxzg42Wyzv0HyM,260
|
||||
PyJWT-1.5.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
jwt/__pycache__/exceptions.cpython-36.pyc,,
|
||||
jwt/__pycache__/algorithms.cpython-36.pyc,,
|
||||
jwt/__pycache__/__main__.cpython-36.pyc,,
|
||||
jwt/__pycache__/compat.cpython-36.pyc,,
|
||||
jwt/__pycache__/utils.cpython-36.pyc,,
|
||||
jwt/__pycache__/__init__.cpython-36.pyc,,
|
||||
jwt/__pycache__/api_jwt.cpython-36.pyc,,
|
||||
jwt/__pycache__/api_jws.cpython-36.pyc,,
|
||||
jwt/contrib/algorithms/__pycache__/py_ecdsa.cpython-36.pyc,,
|
||||
jwt/contrib/algorithms/__pycache__/pycrypto.cpython-36.pyc,,
|
||||
jwt/contrib/algorithms/__pycache__/__init__.cpython-36.pyc,,
|
||||
jwt/contrib/__pycache__/__init__.cpython-36.pyc,,
|
||||
@@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.29.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[console_scripts]
|
||||
pyjwt = jwt.__main__:main
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Utilities"], "description_content_type": "UNKNOWN", "extensions": {"python.commands": {"wrap_console": {"pyjwt": "jwt.__main__:main"}}, "python.details": {"contacts": [{"email": "hello@jpadilla.com", "name": "Jose Padilla", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/jpadilla/pyjwt"}}, "python.exports": {"console_scripts": {"pyjwt": "jwt.__main__:main"}}}, "extras": ["crypto", "flake8", "test"], "generator": "bdist_wheel (0.29.0)", "keywords": ["jwt", "json", "web", "token", "security", "signing"], "license": "MIT", "metadata_version": "2.0", "name": "PyJWT", "run_requires": [{"extra": "crypto", "requires": ["cryptography (>=1.4)"]}, {"extra": "flake8", "requires": ["flake8-import-order", "flake8", "pep8-naming"]}, {"extra": "test", "requires": ["pytest (<4,>3)", "pytest-cov", "pytest-runner"]}], "summary": "JSON Web Token implementation in Python", "test_requires": [{"requires": ["pytest (<4,>3)", "pytest-cov", "pytest-runner"]}], "version": "1.5.3"}
|
||||
@@ -0,0 +1 @@
|
||||
jwt
|
||||
Binary file not shown.
@@ -0,0 +1,851 @@
|
||||
===================================================
|
||||
defusedxml -- defusing XML bombs and other exploits
|
||||
===================================================
|
||||
|
||||
"It's just XML, what could probably go wrong?"
|
||||
|
||||
Christian Heimes <christian@python.org>
|
||||
|
||||
Synopsis
|
||||
========
|
||||
|
||||
The results of an attack on a vulnerable XML library can be fairly dramatic.
|
||||
With just a few hundred **Bytes** of XML data an attacker can occupy several
|
||||
**Gigabytes** of memory within **seconds**. An attacker can also keep
|
||||
CPUs busy for a long time with a small to medium size request. Under some
|
||||
circumstances it is even possible to access local files on your
|
||||
server, to circumvent a firewall, or to abuse services to rebound attacks to
|
||||
third parties.
|
||||
|
||||
The attacks use and abuse less common features of XML and its parsers. The
|
||||
majority of developers are unacquainted with features such as processing
|
||||
instructions and entity expansions that XML inherited from SGML. At best
|
||||
they know about ``<!DOCTYPE>`` from experience with HTML but they are not
|
||||
aware that a document type definition (DTD) can generate an HTTP request
|
||||
or load a file from the file system.
|
||||
|
||||
None of the issues is new. They have been known for a long time. Billion
|
||||
laughs was first reported in 2003. Nevertheless some XML libraries and
|
||||
applications are still vulnerable and even heavy users of XML are
|
||||
surprised by these features. It's hard to say whom to blame for the
|
||||
situation. It's too short sighted to shift all blame on XML parsers and
|
||||
XML libraries for using insecure default settings. After all they
|
||||
properly implement XML specifications. Application developers must not rely
|
||||
that a library is always configured for security and potential harmful data
|
||||
by default.
|
||||
|
||||
|
||||
.. contents:: Table of Contents
|
||||
:depth: 2
|
||||
|
||||
|
||||
Attack vectors
|
||||
==============
|
||||
|
||||
billion laughs / exponential entity expansion
|
||||
---------------------------------------------
|
||||
|
||||
The `Billion Laughs`_ attack -- also known as exponential entity expansion --
|
||||
uses multiple levels of nested entities. The original example uses 9 levels
|
||||
of 10 expansions in each level to expand the string ``lol`` to a string of
|
||||
3 * 10 :sup:`9` bytes, hence the name "billion laughs". The resulting string
|
||||
occupies 3 GB (2.79 GiB) of memory; intermediate strings require additional
|
||||
memory. Because most parsers don't cache the intermediate step for every
|
||||
expansion it is repeated over and over again. It increases the CPU load even
|
||||
more.
|
||||
|
||||
An XML document of just a few hundred bytes can disrupt all services on a
|
||||
machine within seconds.
|
||||
|
||||
Example XML::
|
||||
|
||||
<!DOCTYPE xmlbomb [
|
||||
<!ENTITY a "1234567890" >
|
||||
<!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;">
|
||||
<!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;">
|
||||
<!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;">
|
||||
]>
|
||||
<bomb>&d;</bomb>
|
||||
|
||||
|
||||
quadratic blowup entity expansion
|
||||
---------------------------------
|
||||
|
||||
A quadratic blowup attack is similar to a `Billion Laughs`_ attack; it abuses
|
||||
entity expansion, too. Instead of nested entities it repeats one large entity
|
||||
with a couple of thousand chars over and over again. The attack isn't as
|
||||
efficient as the exponential case but it avoids triggering countermeasures of
|
||||
parsers against heavily nested entities. Some parsers limit the depth and
|
||||
breadth of a single entity but not the total amount of expanded text
|
||||
throughout an entire XML document.
|
||||
|
||||
A medium-sized XML document with a couple of hundred kilobytes can require a
|
||||
couple of hundred MB to several GB of memory. When the attack is combined
|
||||
with some level of nested expansion an attacker is able to achieve a higher
|
||||
ratio of success.
|
||||
|
||||
::
|
||||
|
||||
<!DOCTYPE bomb [
|
||||
<!ENTITY a "xxxxxxx... a couple of ten thousand chars">
|
||||
]>
|
||||
<bomb>&a;&a;&a;... repeat</bomb>
|
||||
|
||||
|
||||
external entity expansion (remote)
|
||||
----------------------------------
|
||||
|
||||
Entity declarations can contain more than just text for replacement. They can
|
||||
also point to external resources by public identifiers or system identifiers.
|
||||
System identifiers are standard URIs. When the URI is a URL (e.g. a
|
||||
``http://`` locator) some parsers download the resource from the remote
|
||||
location and embed them into the XML document verbatim.
|
||||
|
||||
Simple example of a parsed external entity::
|
||||
|
||||
<!DOCTYPE external [
|
||||
<!ENTITY ee SYSTEM "http://www.python.org/some.xml">
|
||||
]>
|
||||
<root>ⅇ</root>
|
||||
|
||||
The case of parsed external entities works only for valid XML content. The
|
||||
XML standard also supports unparsed external entities with a
|
||||
``NData declaration``.
|
||||
|
||||
External entity expansion opens the door to plenty of exploits. An attacker
|
||||
can abuse a vulnerable XML library and application to rebound and forward
|
||||
network requests with the IP address of the server. It highly depends
|
||||
on the parser and the application what kind of exploit is possible. For
|
||||
example:
|
||||
|
||||
* An attacker can circumvent firewalls and gain access to restricted
|
||||
resources as all the requests are made from an internal and trustworthy
|
||||
IP address, not from the outside.
|
||||
* An attacker can abuse a service to attack, spy on or DoS your servers but
|
||||
also third party services. The attack is disguised with the IP address of
|
||||
the server and the attacker is able to utilize the high bandwidth of a big
|
||||
machine.
|
||||
* An attacker can exhaust additional resources on the machine, e.g. with
|
||||
requests to a service that doesn't respond or responds with very large
|
||||
files.
|
||||
* An attacker may gain knowledge, when, how often and from which IP address
|
||||
a XML document is accessed.
|
||||
* An attacker could send mail from inside your network if the URL handler
|
||||
supports ``smtp://`` URIs.
|
||||
|
||||
|
||||
external entity expansion (local file)
|
||||
--------------------------------------
|
||||
|
||||
External entities with references to local files are a sub-case of external
|
||||
entity expansion. It's listed as an extra attack because it deserves extra
|
||||
attention. Some XML libraries such as lxml disable network access by default
|
||||
but still allow entity expansion with local file access by default. Local
|
||||
files are either referenced with a ``file://`` URL or by a file path (either
|
||||
relative or absolute).
|
||||
|
||||
An attacker may be able to access and download all files that can be read by
|
||||
the application process. This may include critical configuration files, too.
|
||||
|
||||
::
|
||||
|
||||
<!DOCTYPE external [
|
||||
<!ENTITY ee SYSTEM "file:///PATH/TO/simple.xml">
|
||||
]>
|
||||
<root>ⅇ</root>
|
||||
|
||||
|
||||
DTD retrieval
|
||||
-------------
|
||||
|
||||
This case is similar to external entity expansion, too. Some XML libraries
|
||||
like Python's xml.dom.pulldom retrieve document type definitions from remote
|
||||
or local locations. Several attack scenarios from the external entity case
|
||||
apply to this issue as well.
|
||||
|
||||
::
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html>
|
||||
<head/>
|
||||
<body>text</body>
|
||||
</html>
|
||||
|
||||
|
||||
Python XML Libraries
|
||||
====================
|
||||
|
||||
.. csv-table:: vulnerabilities and features
|
||||
:header: "kind", "sax", "etree", "minidom", "pulldom", "xmlrpc", "lxml", "genshi"
|
||||
:widths: 24, 7, 8, 8, 7, 8, 8, 8
|
||||
:stub-columns: 0
|
||||
|
||||
"billion laughs", "**True**", "**True**", "**True**", "**True**", "**True**", "False (1)", "False (5)"
|
||||
"quadratic blowup", "**True**", "**True**", "**True**", "**True**", "**True**", "**True**", "False (5)"
|
||||
"external entity expansion (remote)", "**True**", "False (3)", "False (4)", "**True**", "false", "False (1)", "False (5)"
|
||||
"external entity expansion (local file)", "**True**", "False (3)", "False (4)", "**True**", "false", "**True**", "False (5)"
|
||||
"DTD retrieval", "**True**", "False", "False", "**True**", "false", "False (1)", "False"
|
||||
"gzip bomb", "False", "False", "False", "False", "**True**", "**partly** (2)", "False"
|
||||
"xpath support (7)", "False", "False", "False", "False", "False", "**True**", "False"
|
||||
"xsl(t) support (7)", "False", "False", "False", "False", "False", "**True**", "False"
|
||||
"xinclude support (7)", "False", "**True** (6)", "False", "False", "False", "**True** (6)", "**True**"
|
||||
"C library", "expat", "expat", "expat", "expat", "expat", "libxml2", "expat"
|
||||
|
||||
1. Lxml is protected against billion laughs attacks and doesn't do network
|
||||
lookups by default.
|
||||
2. libxml2 and lxml are not directly vulnerable to gzip decompression bombs
|
||||
but they don't protect you against them either.
|
||||
3. xml.etree doesn't expand entities and raises a ParserError when an entity
|
||||
occurs.
|
||||
4. minidom doesn't expand entities and simply returns the unexpanded entity
|
||||
verbatim.
|
||||
5. genshi.input of genshi 0.6 doesn't support entity expansion and raises a
|
||||
ParserError when an entity occurs.
|
||||
6. Library has (limited) XInclude support but requires an additional step to
|
||||
process inclusion.
|
||||
7. These are features but they may introduce exploitable holes, see
|
||||
`Other things to consider`_
|
||||
|
||||
|
||||
Settings in standard library
|
||||
----------------------------
|
||||
|
||||
|
||||
xml.sax.handler Features
|
||||
........................
|
||||
|
||||
feature_external_ges (http://xml.org/sax/features/external-general-entities)
|
||||
disables external entity expansion
|
||||
|
||||
feature_external_pes (http://xml.org/sax/features/external-parameter-entities)
|
||||
the option is ignored and doesn't modify any functionality
|
||||
|
||||
DOM xml.dom.xmlbuilder.Options
|
||||
..............................
|
||||
|
||||
external_parameter_entities
|
||||
ignored
|
||||
|
||||
external_general_entities
|
||||
ignored
|
||||
|
||||
external_dtd_subset
|
||||
ignored
|
||||
|
||||
entities
|
||||
unsure
|
||||
|
||||
|
||||
defusedxml
|
||||
==========
|
||||
|
||||
The `defusedxml package`_ (`defusedxml on PyPI`_)
|
||||
contains several Python-only workarounds and fixes
|
||||
for denial of service and other vulnerabilities in Python's XML libraries.
|
||||
In order to benefit from the protection you just have to import and use the
|
||||
listed functions / classes from the right defusedxml module instead of the
|
||||
original module. Merely `defusedxml.xmlrpc`_ is implemented as monkey patch.
|
||||
|
||||
Instead of::
|
||||
|
||||
>>> from xml.etree.ElementTree import parse
|
||||
>>> et = parse(xmlfile)
|
||||
|
||||
alter code to::
|
||||
|
||||
>>> from defusedxml.ElementTree import parse
|
||||
>>> et = parse(xmlfile)
|
||||
|
||||
Additionally the package has an **untested** function to monkey patch
|
||||
all stdlib modules with ``defusedxml.defuse_stdlib()``.
|
||||
|
||||
All functions and parser classes accept three additional keyword arguments.
|
||||
They return either the same objects as the original functions or compatible
|
||||
subclasses.
|
||||
|
||||
forbid_dtd (default: False)
|
||||
disallow XML with a ``<!DOCTYPE>`` processing instruction and raise a
|
||||
*DTDForbidden* exception when a DTD processing instruction is found.
|
||||
|
||||
forbid_entities (default: True)
|
||||
disallow XML with ``<!ENTITY>`` declarations inside the DTD and raise an
|
||||
*EntitiesForbidden* exception when an entity is declared.
|
||||
|
||||
forbid_external (default: True)
|
||||
disallow any access to remote or local resources in external entities
|
||||
or DTD and raising an *ExternalReferenceForbidden* exception when a DTD
|
||||
or entity references an external resource.
|
||||
|
||||
|
||||
defusedxml (package)
|
||||
--------------------
|
||||
|
||||
DefusedXmlException, DTDForbidden, EntitiesForbidden,
|
||||
ExternalReferenceForbidden, NotSupportedError
|
||||
|
||||
defuse_stdlib() (*experimental*)
|
||||
|
||||
|
||||
defusedxml.cElementTree
|
||||
-----------------------
|
||||
|
||||
parse(), iterparse(), fromstring(), XMLParser
|
||||
|
||||
|
||||
defusedxml.ElementTree
|
||||
-----------------------
|
||||
|
||||
parse(), iterparse(), fromstring(), XMLParser
|
||||
|
||||
|
||||
defusedxml.expatreader
|
||||
----------------------
|
||||
|
||||
create_parser(), DefusedExpatParser
|
||||
|
||||
|
||||
defusedxml.sax
|
||||
--------------
|
||||
|
||||
parse(), parseString(), create_parser()
|
||||
|
||||
|
||||
defusedxml.expatbuilder
|
||||
-----------------------
|
||||
|
||||
parse(), parseString(), DefusedExpatBuilder, DefusedExpatBuilderNS
|
||||
|
||||
|
||||
defusedxml.minidom
|
||||
------------------
|
||||
|
||||
parse(), parseString()
|
||||
|
||||
|
||||
defusedxml.pulldom
|
||||
------------------
|
||||
|
||||
parse(), parseString()
|
||||
|
||||
|
||||
defusedxml.xmlrpc
|
||||
-----------------
|
||||
|
||||
The fix is implemented as monkey patch for the stdlib's xmlrpc package (3.x)
|
||||
or xmlrpclib module (2.x). The function `monkey_patch()` enables the fixes,
|
||||
`unmonkey_patch()` removes the patch and puts the code in its former state.
|
||||
|
||||
The monkey patch protects against XML related attacks as well as
|
||||
decompression bombs and excessively large requests or responses. The default
|
||||
setting is 30 MB for requests, responses and gzip decompression. You can
|
||||
modify the default by changing the module variable `MAX_DATA`. A value of
|
||||
`-1` disables the limit.
|
||||
|
||||
|
||||
defusedxml.lxml
|
||||
---------------
|
||||
|
||||
The module acts as an *example* how you could protect code that uses
|
||||
lxml.etree. It implements a custom Element class that filters out
|
||||
Entity instances, a custom parser factory and a thread local storage for
|
||||
parser instances. It also has a check_docinfo() function which inspects
|
||||
a tree for internal or external DTDs and entity declarations. In order to
|
||||
check for entities lxml > 3.0 is required.
|
||||
|
||||
parse(), fromstring()
|
||||
RestrictedElement, GlobalParserTLS, getDefaultParser(), check_docinfo()
|
||||
|
||||
|
||||
defusedexpat
|
||||
============
|
||||
|
||||
The `defusedexpat package`_ (`defusedexpat on PyPI`_)
|
||||
comes with binary extensions and a
|
||||
`modified expat`_ libary instead of the standard `expat parser`_. It's
|
||||
basically a stand-alone version of the patches for Python's standard
|
||||
library C extensions.
|
||||
|
||||
Modifications in expat
|
||||
----------------------
|
||||
|
||||
new definitions::
|
||||
|
||||
XML_BOMB_PROTECTION
|
||||
XML_DEFAULT_MAX_ENTITY_INDIRECTIONS
|
||||
XML_DEFAULT_MAX_ENTITY_EXPANSIONS
|
||||
XML_DEFAULT_RESET_DTD
|
||||
|
||||
new XML_FeatureEnum members::
|
||||
|
||||
XML_FEATURE_MAX_ENTITY_INDIRECTIONS
|
||||
XML_FEATURE_MAX_ENTITY_EXPANSIONS
|
||||
XML_FEATURE_IGNORE_DTD
|
||||
|
||||
new XML_Error members::
|
||||
|
||||
XML_ERROR_ENTITY_INDIRECTIONS
|
||||
XML_ERROR_ENTITY_EXPANSION
|
||||
|
||||
new API functions::
|
||||
|
||||
int XML_GetFeature(XML_Parser parser,
|
||||
enum XML_FeatureEnum feature,
|
||||
long *value);
|
||||
int XML_SetFeature(XML_Parser parser,
|
||||
enum XML_FeatureEnum feature,
|
||||
long value);
|
||||
int XML_GetFeatureDefault(enum XML_FeatureEnum feature,
|
||||
long *value);
|
||||
int XML_SetFeatureDefault(enum XML_FeatureEnum feature,
|
||||
long value);
|
||||
|
||||
XML_FEATURE_MAX_ENTITY_INDIRECTIONS
|
||||
Limit the amount of indirections that are allowed to occur during the
|
||||
expansion of a nested entity. A counter starts when an entity reference
|
||||
is encountered. It resets after the entity is fully expanded. The limit
|
||||
protects the parser against exponential entity expansion attacks (aka
|
||||
billion laughs attack). When the limit is exceeded the parser stops and
|
||||
fails with `XML_ERROR_ENTITY_INDIRECTIONS`.
|
||||
A value of 0 disables the protection.
|
||||
|
||||
Supported range
|
||||
0 .. UINT_MAX
|
||||
Default
|
||||
40
|
||||
|
||||
XML_FEATURE_MAX_ENTITY_EXPANSIONS
|
||||
Limit the total length of all entity expansions throughout the entire
|
||||
document. The lengths of all entities are accumulated in a parser variable.
|
||||
The setting protects against quadratic blowup attacks (lots of expansions
|
||||
of a large entity declaration). When the sum of all entities exceeds
|
||||
the limit, the parser stops and fails with `XML_ERROR_ENTITY_EXPANSION`.
|
||||
A value of 0 disables the protection.
|
||||
|
||||
Supported range
|
||||
0 .. UINT_MAX
|
||||
Default
|
||||
8 MiB
|
||||
|
||||
XML_FEATURE_RESET_DTD
|
||||
Reset all DTD information after the <!DOCTYPE> block has been parsed. When
|
||||
the flag is set (default: false) all DTD information after the
|
||||
endDoctypeDeclHandler has been called. The flag can be set inside the
|
||||
endDoctypeDeclHandler. Without DTD information any entity reference in
|
||||
the document body leads to `XML_ERROR_UNDEFINED_ENTITY`.
|
||||
|
||||
Supported range
|
||||
0, 1
|
||||
Default
|
||||
0
|
||||
|
||||
|
||||
How to avoid XML vulnerabilities
|
||||
================================
|
||||
|
||||
Best practices
|
||||
--------------
|
||||
|
||||
* Don't allow DTDs
|
||||
* Don't expand entities
|
||||
* Don't resolve externals
|
||||
* Limit parse depth
|
||||
* Limit total input size
|
||||
* Limit parse time
|
||||
* Favor a SAX or iterparse-like parser for potential large data
|
||||
* Validate and properly quote arguments to XSL transformations and
|
||||
XPath queries
|
||||
* Don't use XPath expression from untrusted sources
|
||||
* Don't apply XSL transformations that come untrusted sources
|
||||
|
||||
(based on Brad Hill's `Attacking XML Security`_)
|
||||
|
||||
|
||||
Other things to consider
|
||||
========================
|
||||
|
||||
XML, XML parsers and processing libraries have more features and possible
|
||||
issue that could lead to DoS vulnerabilities or security exploits in
|
||||
applications. I have compiled an incomplete list of theoretical issues that
|
||||
need further research and more attention. The list is deliberately pessimistic
|
||||
and a bit paranoid, too. It contains things that might go wrong under daffy
|
||||
circumstances.
|
||||
|
||||
|
||||
attribute blowup / hash collision attack
|
||||
----------------------------------------
|
||||
|
||||
XML parsers may use an algorithm with quadratic runtime O(n :sup:`2`) to
|
||||
handle attributes and namespaces. If it uses hash tables (dictionaries) to
|
||||
store attributes and namespaces the implementation may be vulnerable to
|
||||
hash collision attacks, thus reducing the performance to O(n :sup:`2`) again.
|
||||
In either case an attacker is able to forge a denial of service attack with
|
||||
an XML document that contains thousands upon thousands of attributes in
|
||||
a single node.
|
||||
|
||||
I haven't researched yet if expat, pyexpat or libxml2 are vulnerable.
|
||||
|
||||
|
||||
decompression bomb
|
||||
------------------
|
||||
|
||||
The issue of decompression bombs (aka `ZIP bomb`_) apply to all XML libraries
|
||||
that can parse compressed XML stream like gzipped HTTP streams or LZMA-ed
|
||||
files. For an attacker it can reduce the amount of transmitted data by three
|
||||
magnitudes or more. Gzip is able to compress 1 GiB zeros to roughly 1 MB,
|
||||
lzma is even better::
|
||||
|
||||
$ dd if=/dev/zero bs=1M count=1024 | gzip > zeros.gz
|
||||
$ dd if=/dev/zero bs=1M count=1024 | lzma -z > zeros.xy
|
||||
$ ls -sh zeros.*
|
||||
1020K zeros.gz
|
||||
148K zeros.xy
|
||||
|
||||
None of Python's standard XML libraries decompress streams except for
|
||||
``xmlrpclib``. The module is vulnerable <http://bugs.python.org/issue16043>
|
||||
to decompression bombs.
|
||||
|
||||
lxml can load and process compressed data through libxml2 transparently.
|
||||
libxml2 can handle even very large blobs of compressed data efficiently
|
||||
without using too much memory. But it doesn't protect applications from
|
||||
decompression bombs. A carefully written SAX or iterparse-like approach can
|
||||
be safe.
|
||||
|
||||
|
||||
Processing Instruction
|
||||
----------------------
|
||||
|
||||
`PI`_'s like::
|
||||
|
||||
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
|
||||
|
||||
may impose more threats for XML processing. It depends if and how a
|
||||
processor handles processing instructions. The issue of URL retrieval with
|
||||
network or local file access apply to processing instructions, too.
|
||||
|
||||
|
||||
Other DTD features
|
||||
------------------
|
||||
|
||||
`DTD`_ has more features like ``<!NOTATION>``. I haven't researched how
|
||||
these features may be a security threat.
|
||||
|
||||
|
||||
XPath
|
||||
-----
|
||||
|
||||
XPath statements may introduce DoS vulnerabilities. Code should never execute
|
||||
queries from untrusted sources. An attacker may also be able to create a XML
|
||||
document that makes certain XPath queries costly or resource hungry.
|
||||
|
||||
|
||||
XPath injection attacks
|
||||
-----------------------
|
||||
|
||||
XPath injeciton attacks pretty much work like SQL injection attacks.
|
||||
Arguments to XPath queries must be quoted and validated properly, especially
|
||||
when they are taken from the user. The page `Avoid the dangers of XPath injection`_
|
||||
list some ramifications of XPath injections.
|
||||
|
||||
Python's standard library doesn't have XPath support. Lxml supports
|
||||
parameterized XPath queries which does proper quoting. You just have to use
|
||||
its xpath() method correctly::
|
||||
|
||||
# DON'T
|
||||
>>> tree.xpath("/tag[@id='%s']" % value)
|
||||
|
||||
# instead do
|
||||
>>> tree.xpath("/tag[@id=$tagid]", tagid=name)
|
||||
|
||||
|
||||
XInclude
|
||||
--------
|
||||
|
||||
`XML Inclusion`_ is another way to load and include external files::
|
||||
|
||||
<root xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<xi:include href="filename.txt" parse="text" />
|
||||
</root>
|
||||
|
||||
This feature should be disabled when XML files from an untrusted source are
|
||||
processed. Some Python XML libraries and libxml2 support XInclude but don't
|
||||
have an option to sandbox inclusion and limit it to allowed directories.
|
||||
|
||||
|
||||
XMLSchema location
|
||||
------------------
|
||||
|
||||
A validating XML parser may download schema files from the information in a
|
||||
``xsi:schemaLocation`` attribute.
|
||||
|
||||
::
|
||||
|
||||
<ead xmlns="urn:isbn:1-931666-22-9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="urn:isbn:1-931666-22-9 http://www.loc.gov/ead/ead.xsd">
|
||||
</ead>
|
||||
|
||||
|
||||
XSL Transformation
|
||||
------------------
|
||||
|
||||
You should keep in mind that XSLT is a Turing complete language. Never
|
||||
process XSLT code from unknown or untrusted source! XSLT processors may
|
||||
allow you to interact with external resources in ways you can't even imagine.
|
||||
Some processors even support extensions that allow read/write access to file
|
||||
system, access to JRE objects or scripting with Jython.
|
||||
|
||||
Example from `Attacking XML Security`_ for Xalan-J::
|
||||
|
||||
<xsl:stylesheet version="1.0"
|
||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime"
|
||||
xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object"
|
||||
exclude-result-prefixes= "rt ob">
|
||||
<xsl:template match="/">
|
||||
<xsl:variable name="runtimeObject" select="rt:getRuntime()"/>
|
||||
<xsl:variable name="command"
|
||||
select="rt:exec($runtimeObject, 'c:\Windows\system32\cmd.exe')"/>
|
||||
<xsl:variable name="commandAsString" select="ob:toString($command)"/>
|
||||
<xsl:value-of select="$commandAsString"/>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
|
||||
|
||||
Related CVEs
|
||||
============
|
||||
|
||||
CVE-2013-1664
|
||||
Unrestricted entity expansion induces DoS vulnerabilities in Python XML
|
||||
libraries (XML bomb)
|
||||
|
||||
CVE-2013-1665
|
||||
External entity expansion in Python XML libraries inflicts potential
|
||||
security flaws and DoS vulnerabilities
|
||||
|
||||
|
||||
Other languages / frameworks
|
||||
=============================
|
||||
|
||||
Several other programming languages and frameworks are vulnerable as well. A
|
||||
couple of them are affected by the fact that libxml2 up to 2.9.0 has no
|
||||
protection against quadratic blowup attacks. Most of them have potential
|
||||
dangerous default settings for entity expansion and external entities, too.
|
||||
|
||||
Perl
|
||||
----
|
||||
|
||||
Perl's XML::Simple is vulnerable to quadratic entity expansion and external
|
||||
entity expansion (both local and remote).
|
||||
|
||||
|
||||
Ruby
|
||||
----
|
||||
|
||||
Ruby's REXML document parser is vulnerable to entity expansion attacks
|
||||
(both quadratic and exponential) but it doesn't do external entity
|
||||
expansion by default. In order to counteract entity expansion you have to
|
||||
disable the feature::
|
||||
|
||||
REXML::Document.entity_expansion_limit = 0
|
||||
|
||||
libxml-ruby and hpricot don't expand entities in their default configuration.
|
||||
|
||||
|
||||
PHP
|
||||
---
|
||||
|
||||
PHP's SimpleXML API is vulnerable to quadratic entity expansion and loads
|
||||
entites from local and remote resources. The option ``LIBXML_NONET`` disables
|
||||
network access but still allows local file access. ``LIBXML_NOENT`` seems to
|
||||
have no effect on entity expansion in PHP 5.4.6.
|
||||
|
||||
|
||||
C# / .NET / Mono
|
||||
----------------
|
||||
|
||||
Information in `XML DoS and Defenses (MSDN)`_ suggest that .NET is
|
||||
vulnerable with its default settings. The article contains code snippets
|
||||
how to create a secure XML reader::
|
||||
|
||||
XmlReaderSettings settings = new XmlReaderSettings();
|
||||
settings.ProhibitDtd = false;
|
||||
settings.MaxCharactersFromEntities = 1024;
|
||||
settings.XmlResolver = null;
|
||||
XmlReader reader = XmlReader.Create(stream, settings);
|
||||
|
||||
|
||||
Java
|
||||
----
|
||||
|
||||
Untested. The documentation of Xerces and its `Xerces SecurityMananger`_
|
||||
sounds like Xerces is also vulnerable to billion laugh attacks with its
|
||||
default settings. It also does entity resolving when an
|
||||
``org.xml.sax.EntityResolver`` is configured. I'm not yet sure about the
|
||||
default setting here.
|
||||
|
||||
Java specialists suggest to have a custom builder factory::
|
||||
|
||||
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
|
||||
builderFactory.setXIncludeAware(False);
|
||||
builderFactory.setExpandEntityReferences(False);
|
||||
builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, True);
|
||||
# either
|
||||
builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", True);
|
||||
# or if you need DTDs
|
||||
builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", False);
|
||||
builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", False);
|
||||
builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", False);
|
||||
builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", False);
|
||||
|
||||
|
||||
TODO
|
||||
====
|
||||
|
||||
* DOM: Use xml.dom.xmlbuilder options for entity handling
|
||||
* SAX: take feature_external_ges and feature_external_pes (?) into account
|
||||
* test experimental monkey patching of stdlib modules
|
||||
* improve documentation
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Copyright (c) 2013-2017 by Christian Heimes <christian@python.org>
|
||||
|
||||
Licensed to PSF under a Contributor Agreement.
|
||||
|
||||
See http://www.python.org/psf/license for licensing details.
|
||||
|
||||
|
||||
Acknowledgements
|
||||
================
|
||||
|
||||
Brett Cannon (Python Core developer)
|
||||
review and code cleanup
|
||||
|
||||
Antoine Pitrou (Python Core developer)
|
||||
code review
|
||||
|
||||
Aaron Patterson, Ben Murphy and Michael Koziarski (Ruby community)
|
||||
Many thanks to Aaron, Ben and Michael from the Ruby community for their
|
||||
report and assistance.
|
||||
|
||||
Thierry Carrez (OpenStack)
|
||||
Many thanks to Thierry for his report to the Python Security Response
|
||||
Team on behalf of the OpenStack security team.
|
||||
|
||||
Carl Meyer (Django)
|
||||
Many thanks to Carl for his report to PSRT on behalf of the Django security
|
||||
team.
|
||||
|
||||
Daniel Veillard (libxml2)
|
||||
Many thanks to Daniel for his insight and assistance with libxml2.
|
||||
|
||||
semantics GmbH (http://www.semantics.de/)
|
||||
Many thanks to my employer semantics for letting me work on the issue
|
||||
during working hours as part of semantics's open source initiative.
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
* `XML DoS and Defenses (MSDN)`_
|
||||
* `Billion Laughs`_ on Wikipedia
|
||||
* `ZIP bomb`_ on Wikipedia
|
||||
* `Configure SAX parsers for secure processing`_
|
||||
* `Testing for XML Injection`_
|
||||
|
||||
.. _defusedxml package: https://bitbucket.org/tiran/defusedxml
|
||||
.. _defusedxml on PyPI: https://pypi.python.org/pypi/defusedxml
|
||||
.. _defusedexpat package: https://bitbucket.org/tiran/defusedexpat
|
||||
.. _defusedexpat on PyPI: https://pypi.python.org/pypi/defusedexpat
|
||||
.. _modified expat: https://bitbucket.org/tiran/expat
|
||||
.. _expat parser: http://expat.sourceforge.net/
|
||||
.. _Attacking XML Security: https://www.isecpartners.com/media/12976/iSEC-HILL-Attacking-XML-Security-bh07.pdf
|
||||
.. _Billion Laughs: http://en.wikipedia.org/wiki/Billion_laughs
|
||||
.. _XML DoS and Defenses (MSDN): http://msdn.microsoft.com/en-us/magazine/ee335713.aspx
|
||||
.. _ZIP bomb: http://en.wikipedia.org/wiki/Zip_bomb
|
||||
.. _DTD: http://en.wikipedia.org/wiki/Document_Type_Definition
|
||||
.. _PI: https://en.wikipedia.org/wiki/Processing_Instruction
|
||||
.. _Avoid the dangers of XPath injection: http://www.ibm.com/developerworks/xml/library/x-xpathinjection/index.html
|
||||
.. _Configure SAX parsers for secure processing: http://www.ibm.com/developerworks/xml/library/x-tipcfsx/index.html
|
||||
.. _Testing for XML Injection: https://www.owasp.org/index.php/Testing_for_XML_Injection_(OWASP-DV-008)
|
||||
.. _Xerces SecurityMananger: http://xerces.apache.org/xerces2-j/javadocs/xerces2/org/apache/xerces/util/SecurityManager.html
|
||||
.. _XML Inclusion: http://www.w3.org/TR/xinclude/#include_element
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
defusedxml 0.5.0
|
||||
----------------
|
||||
|
||||
*Release date: 07-Feb-2017*
|
||||
|
||||
- No changes
|
||||
|
||||
defusedxml 0.5.0.rc1
|
||||
--------------------
|
||||
|
||||
*Release date: 28-Jan-2017*
|
||||
|
||||
- Add compatibility with Python 3.6
|
||||
- Drop support for Python 2.6, 3.1, 3.2, 3.3
|
||||
- Fix lxml tests (XMLSyntaxError: Detected an entity reference loop)
|
||||
|
||||
|
||||
defusedxml 0.4.1
|
||||
----------------
|
||||
|
||||
*Release date: 28-Mar-2013*
|
||||
|
||||
- Add more demo exploits, e.g. python_external.py and Xalan XSLT demos.
|
||||
- Improved documentation.
|
||||
|
||||
|
||||
defusedxml 0.4
|
||||
--------------
|
||||
|
||||
*Release date: 25-Feb-2013*
|
||||
|
||||
- As per http://seclists.org/oss-sec/2013/q1/340 please REJECT
|
||||
CVE-2013-0278, CVE-2013-0279 and CVE-2013-0280 and use CVE-2013-1664,
|
||||
CVE-2013-1665 for OpenStack/etc.
|
||||
- Add missing parser_list argument to sax.make_parser(). The argument is
|
||||
ignored, though. (thanks to Florian Apolloner)
|
||||
- Add demo exploit for external entity attack on Python's SAX parser, XML-RPC
|
||||
and WebDAV.
|
||||
|
||||
|
||||
defusedxml 0.3
|
||||
--------------
|
||||
|
||||
*Release date: 19-Feb-2013*
|
||||
|
||||
- Improve documentation
|
||||
|
||||
|
||||
defusedxml 0.2
|
||||
--------------
|
||||
|
||||
*Release date: 15-Feb-2013*
|
||||
|
||||
- Rename ExternalEntitiesForbidden to ExternalReferenceForbidden
|
||||
- Rename defusedxml.lxml.check_dtd() to check_docinfo()
|
||||
- Unify argument names in callbacks
|
||||
- Add arguments and formatted representation to exceptions
|
||||
- Add forbid_external argument to all functions and classs
|
||||
- More tests
|
||||
- LOTS of documentation
|
||||
- Add example code for other languages (Ruby, Perl, PHP) and parsers (Genshi)
|
||||
- Add protection against XML and gzip attacks to xmlrpclib
|
||||
|
||||
defusedxml 0.1
|
||||
--------------
|
||||
|
||||
*Release date: 08-Feb-2013*
|
||||
|
||||
- Initial and internal release for PSRT review
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,875 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: defusedxml
|
||||
Version: 0.5.0
|
||||
Summary: XML bomb protection for Python stdlib modules
|
||||
Home-page: https://github.com/tiran/defusedxml
|
||||
Author: Christian Heimes
|
||||
Author-email: christian@python.org
|
||||
License: PSFL
|
||||
Download-URL: https://pypi.python.org/pypi/defusedxml
|
||||
Keywords: xml bomb DoS
|
||||
Platform: all
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: Python Software Foundation License
|
||||
Classifier: Natural Language :: English
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Topic :: Text Processing :: Markup :: XML
|
||||
|
||||
===================================================
|
||||
defusedxml -- defusing XML bombs and other exploits
|
||||
===================================================
|
||||
|
||||
"It's just XML, what could probably go wrong?"
|
||||
|
||||
Christian Heimes <christian@python.org>
|
||||
|
||||
Synopsis
|
||||
========
|
||||
|
||||
The results of an attack on a vulnerable XML library can be fairly dramatic.
|
||||
With just a few hundred **Bytes** of XML data an attacker can occupy several
|
||||
**Gigabytes** of memory within **seconds**. An attacker can also keep
|
||||
CPUs busy for a long time with a small to medium size request. Under some
|
||||
circumstances it is even possible to access local files on your
|
||||
server, to circumvent a firewall, or to abuse services to rebound attacks to
|
||||
third parties.
|
||||
|
||||
The attacks use and abuse less common features of XML and its parsers. The
|
||||
majority of developers are unacquainted with features such as processing
|
||||
instructions and entity expansions that XML inherited from SGML. At best
|
||||
they know about ``<!DOCTYPE>`` from experience with HTML but they are not
|
||||
aware that a document type definition (DTD) can generate an HTTP request
|
||||
or load a file from the file system.
|
||||
|
||||
None of the issues is new. They have been known for a long time. Billion
|
||||
laughs was first reported in 2003. Nevertheless some XML libraries and
|
||||
applications are still vulnerable and even heavy users of XML are
|
||||
surprised by these features. It's hard to say whom to blame for the
|
||||
situation. It's too short sighted to shift all blame on XML parsers and
|
||||
XML libraries for using insecure default settings. After all they
|
||||
properly implement XML specifications. Application developers must not rely
|
||||
that a library is always configured for security and potential harmful data
|
||||
by default.
|
||||
|
||||
|
||||
.. contents:: Table of Contents
|
||||
:depth: 2
|
||||
|
||||
|
||||
Attack vectors
|
||||
==============
|
||||
|
||||
billion laughs / exponential entity expansion
|
||||
---------------------------------------------
|
||||
|
||||
The `Billion Laughs`_ attack -- also known as exponential entity expansion --
|
||||
uses multiple levels of nested entities. The original example uses 9 levels
|
||||
of 10 expansions in each level to expand the string ``lol`` to a string of
|
||||
3 * 10 :sup:`9` bytes, hence the name "billion laughs". The resulting string
|
||||
occupies 3 GB (2.79 GiB) of memory; intermediate strings require additional
|
||||
memory. Because most parsers don't cache the intermediate step for every
|
||||
expansion it is repeated over and over again. It increases the CPU load even
|
||||
more.
|
||||
|
||||
An XML document of just a few hundred bytes can disrupt all services on a
|
||||
machine within seconds.
|
||||
|
||||
Example XML::
|
||||
|
||||
<!DOCTYPE xmlbomb [
|
||||
<!ENTITY a "1234567890" >
|
||||
<!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;">
|
||||
<!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;">
|
||||
<!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;">
|
||||
]>
|
||||
<bomb>&d;</bomb>
|
||||
|
||||
|
||||
quadratic blowup entity expansion
|
||||
---------------------------------
|
||||
|
||||
A quadratic blowup attack is similar to a `Billion Laughs`_ attack; it abuses
|
||||
entity expansion, too. Instead of nested entities it repeats one large entity
|
||||
with a couple of thousand chars over and over again. The attack isn't as
|
||||
efficient as the exponential case but it avoids triggering countermeasures of
|
||||
parsers against heavily nested entities. Some parsers limit the depth and
|
||||
breadth of a single entity but not the total amount of expanded text
|
||||
throughout an entire XML document.
|
||||
|
||||
A medium-sized XML document with a couple of hundred kilobytes can require a
|
||||
couple of hundred MB to several GB of memory. When the attack is combined
|
||||
with some level of nested expansion an attacker is able to achieve a higher
|
||||
ratio of success.
|
||||
|
||||
::
|
||||
|
||||
<!DOCTYPE bomb [
|
||||
<!ENTITY a "xxxxxxx... a couple of ten thousand chars">
|
||||
]>
|
||||
<bomb>&a;&a;&a;... repeat</bomb>
|
||||
|
||||
|
||||
external entity expansion (remote)
|
||||
----------------------------------
|
||||
|
||||
Entity declarations can contain more than just text for replacement. They can
|
||||
also point to external resources by public identifiers or system identifiers.
|
||||
System identifiers are standard URIs. When the URI is a URL (e.g. a
|
||||
``http://`` locator) some parsers download the resource from the remote
|
||||
location and embed them into the XML document verbatim.
|
||||
|
||||
Simple example of a parsed external entity::
|
||||
|
||||
<!DOCTYPE external [
|
||||
<!ENTITY ee SYSTEM "http://www.python.org/some.xml">
|
||||
]>
|
||||
<root>ⅇ</root>
|
||||
|
||||
The case of parsed external entities works only for valid XML content. The
|
||||
XML standard also supports unparsed external entities with a
|
||||
``NData declaration``.
|
||||
|
||||
External entity expansion opens the door to plenty of exploits. An attacker
|
||||
can abuse a vulnerable XML library and application to rebound and forward
|
||||
network requests with the IP address of the server. It highly depends
|
||||
on the parser and the application what kind of exploit is possible. For
|
||||
example:
|
||||
|
||||
* An attacker can circumvent firewalls and gain access to restricted
|
||||
resources as all the requests are made from an internal and trustworthy
|
||||
IP address, not from the outside.
|
||||
* An attacker can abuse a service to attack, spy on or DoS your servers but
|
||||
also third party services. The attack is disguised with the IP address of
|
||||
the server and the attacker is able to utilize the high bandwidth of a big
|
||||
machine.
|
||||
* An attacker can exhaust additional resources on the machine, e.g. with
|
||||
requests to a service that doesn't respond or responds with very large
|
||||
files.
|
||||
* An attacker may gain knowledge, when, how often and from which IP address
|
||||
a XML document is accessed.
|
||||
* An attacker could send mail from inside your network if the URL handler
|
||||
supports ``smtp://`` URIs.
|
||||
|
||||
|
||||
external entity expansion (local file)
|
||||
--------------------------------------
|
||||
|
||||
External entities with references to local files are a sub-case of external
|
||||
entity expansion. It's listed as an extra attack because it deserves extra
|
||||
attention. Some XML libraries such as lxml disable network access by default
|
||||
but still allow entity expansion with local file access by default. Local
|
||||
files are either referenced with a ``file://`` URL or by a file path (either
|
||||
relative or absolute).
|
||||
|
||||
An attacker may be able to access and download all files that can be read by
|
||||
the application process. This may include critical configuration files, too.
|
||||
|
||||
::
|
||||
|
||||
<!DOCTYPE external [
|
||||
<!ENTITY ee SYSTEM "file:///PATH/TO/simple.xml">
|
||||
]>
|
||||
<root>ⅇ</root>
|
||||
|
||||
|
||||
DTD retrieval
|
||||
-------------
|
||||
|
||||
This case is similar to external entity expansion, too. Some XML libraries
|
||||
like Python's xml.dom.pulldom retrieve document type definitions from remote
|
||||
or local locations. Several attack scenarios from the external entity case
|
||||
apply to this issue as well.
|
||||
|
||||
::
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html>
|
||||
<head/>
|
||||
<body>text</body>
|
||||
</html>
|
||||
|
||||
|
||||
Python XML Libraries
|
||||
====================
|
||||
|
||||
.. csv-table:: vulnerabilities and features
|
||||
:header: "kind", "sax", "etree", "minidom", "pulldom", "xmlrpc", "lxml", "genshi"
|
||||
:widths: 24, 7, 8, 8, 7, 8, 8, 8
|
||||
:stub-columns: 0
|
||||
|
||||
"billion laughs", "**True**", "**True**", "**True**", "**True**", "**True**", "False (1)", "False (5)"
|
||||
"quadratic blowup", "**True**", "**True**", "**True**", "**True**", "**True**", "**True**", "False (5)"
|
||||
"external entity expansion (remote)", "**True**", "False (3)", "False (4)", "**True**", "false", "False (1)", "False (5)"
|
||||
"external entity expansion (local file)", "**True**", "False (3)", "False (4)", "**True**", "false", "**True**", "False (5)"
|
||||
"DTD retrieval", "**True**", "False", "False", "**True**", "false", "False (1)", "False"
|
||||
"gzip bomb", "False", "False", "False", "False", "**True**", "**partly** (2)", "False"
|
||||
"xpath support (7)", "False", "False", "False", "False", "False", "**True**", "False"
|
||||
"xsl(t) support (7)", "False", "False", "False", "False", "False", "**True**", "False"
|
||||
"xinclude support (7)", "False", "**True** (6)", "False", "False", "False", "**True** (6)", "**True**"
|
||||
"C library", "expat", "expat", "expat", "expat", "expat", "libxml2", "expat"
|
||||
|
||||
1. Lxml is protected against billion laughs attacks and doesn't do network
|
||||
lookups by default.
|
||||
2. libxml2 and lxml are not directly vulnerable to gzip decompression bombs
|
||||
but they don't protect you against them either.
|
||||
3. xml.etree doesn't expand entities and raises a ParserError when an entity
|
||||
occurs.
|
||||
4. minidom doesn't expand entities and simply returns the unexpanded entity
|
||||
verbatim.
|
||||
5. genshi.input of genshi 0.6 doesn't support entity expansion and raises a
|
||||
ParserError when an entity occurs.
|
||||
6. Library has (limited) XInclude support but requires an additional step to
|
||||
process inclusion.
|
||||
7. These are features but they may introduce exploitable holes, see
|
||||
`Other things to consider`_
|
||||
|
||||
|
||||
Settings in standard library
|
||||
----------------------------
|
||||
|
||||
|
||||
xml.sax.handler Features
|
||||
........................
|
||||
|
||||
feature_external_ges (http://xml.org/sax/features/external-general-entities)
|
||||
disables external entity expansion
|
||||
|
||||
feature_external_pes (http://xml.org/sax/features/external-parameter-entities)
|
||||
the option is ignored and doesn't modify any functionality
|
||||
|
||||
DOM xml.dom.xmlbuilder.Options
|
||||
..............................
|
||||
|
||||
external_parameter_entities
|
||||
ignored
|
||||
|
||||
external_general_entities
|
||||
ignored
|
||||
|
||||
external_dtd_subset
|
||||
ignored
|
||||
|
||||
entities
|
||||
unsure
|
||||
|
||||
|
||||
defusedxml
|
||||
==========
|
||||
|
||||
The `defusedxml package`_ (`defusedxml on PyPI`_)
|
||||
contains several Python-only workarounds and fixes
|
||||
for denial of service and other vulnerabilities in Python's XML libraries.
|
||||
In order to benefit from the protection you just have to import and use the
|
||||
listed functions / classes from the right defusedxml module instead of the
|
||||
original module. Merely `defusedxml.xmlrpc`_ is implemented as monkey patch.
|
||||
|
||||
Instead of::
|
||||
|
||||
>>> from xml.etree.ElementTree import parse
|
||||
>>> et = parse(xmlfile)
|
||||
|
||||
alter code to::
|
||||
|
||||
>>> from defusedxml.ElementTree import parse
|
||||
>>> et = parse(xmlfile)
|
||||
|
||||
Additionally the package has an **untested** function to monkey patch
|
||||
all stdlib modules with ``defusedxml.defuse_stdlib()``.
|
||||
|
||||
All functions and parser classes accept three additional keyword arguments.
|
||||
They return either the same objects as the original functions or compatible
|
||||
subclasses.
|
||||
|
||||
forbid_dtd (default: False)
|
||||
disallow XML with a ``<!DOCTYPE>`` processing instruction and raise a
|
||||
*DTDForbidden* exception when a DTD processing instruction is found.
|
||||
|
||||
forbid_entities (default: True)
|
||||
disallow XML with ``<!ENTITY>`` declarations inside the DTD and raise an
|
||||
*EntitiesForbidden* exception when an entity is declared.
|
||||
|
||||
forbid_external (default: True)
|
||||
disallow any access to remote or local resources in external entities
|
||||
or DTD and raising an *ExternalReferenceForbidden* exception when a DTD
|
||||
or entity references an external resource.
|
||||
|
||||
|
||||
defusedxml (package)
|
||||
--------------------
|
||||
|
||||
DefusedXmlException, DTDForbidden, EntitiesForbidden,
|
||||
ExternalReferenceForbidden, NotSupportedError
|
||||
|
||||
defuse_stdlib() (*experimental*)
|
||||
|
||||
|
||||
defusedxml.cElementTree
|
||||
-----------------------
|
||||
|
||||
parse(), iterparse(), fromstring(), XMLParser
|
||||
|
||||
|
||||
defusedxml.ElementTree
|
||||
-----------------------
|
||||
|
||||
parse(), iterparse(), fromstring(), XMLParser
|
||||
|
||||
|
||||
defusedxml.expatreader
|
||||
----------------------
|
||||
|
||||
create_parser(), DefusedExpatParser
|
||||
|
||||
|
||||
defusedxml.sax
|
||||
--------------
|
||||
|
||||
parse(), parseString(), create_parser()
|
||||
|
||||
|
||||
defusedxml.expatbuilder
|
||||
-----------------------
|
||||
|
||||
parse(), parseString(), DefusedExpatBuilder, DefusedExpatBuilderNS
|
||||
|
||||
|
||||
defusedxml.minidom
|
||||
------------------
|
||||
|
||||
parse(), parseString()
|
||||
|
||||
|
||||
defusedxml.pulldom
|
||||
------------------
|
||||
|
||||
parse(), parseString()
|
||||
|
||||
|
||||
defusedxml.xmlrpc
|
||||
-----------------
|
||||
|
||||
The fix is implemented as monkey patch for the stdlib's xmlrpc package (3.x)
|
||||
or xmlrpclib module (2.x). The function `monkey_patch()` enables the fixes,
|
||||
`unmonkey_patch()` removes the patch and puts the code in its former state.
|
||||
|
||||
The monkey patch protects against XML related attacks as well as
|
||||
decompression bombs and excessively large requests or responses. The default
|
||||
setting is 30 MB for requests, responses and gzip decompression. You can
|
||||
modify the default by changing the module variable `MAX_DATA`. A value of
|
||||
`-1` disables the limit.
|
||||
|
||||
|
||||
defusedxml.lxml
|
||||
---------------
|
||||
|
||||
The module acts as an *example* how you could protect code that uses
|
||||
lxml.etree. It implements a custom Element class that filters out
|
||||
Entity instances, a custom parser factory and a thread local storage for
|
||||
parser instances. It also has a check_docinfo() function which inspects
|
||||
a tree for internal or external DTDs and entity declarations. In order to
|
||||
check for entities lxml > 3.0 is required.
|
||||
|
||||
parse(), fromstring()
|
||||
RestrictedElement, GlobalParserTLS, getDefaultParser(), check_docinfo()
|
||||
|
||||
|
||||
defusedexpat
|
||||
============
|
||||
|
||||
The `defusedexpat package`_ (`defusedexpat on PyPI`_)
|
||||
comes with binary extensions and a
|
||||
`modified expat`_ libary instead of the standard `expat parser`_. It's
|
||||
basically a stand-alone version of the patches for Python's standard
|
||||
library C extensions.
|
||||
|
||||
Modifications in expat
|
||||
----------------------
|
||||
|
||||
new definitions::
|
||||
|
||||
XML_BOMB_PROTECTION
|
||||
XML_DEFAULT_MAX_ENTITY_INDIRECTIONS
|
||||
XML_DEFAULT_MAX_ENTITY_EXPANSIONS
|
||||
XML_DEFAULT_RESET_DTD
|
||||
|
||||
new XML_FeatureEnum members::
|
||||
|
||||
XML_FEATURE_MAX_ENTITY_INDIRECTIONS
|
||||
XML_FEATURE_MAX_ENTITY_EXPANSIONS
|
||||
XML_FEATURE_IGNORE_DTD
|
||||
|
||||
new XML_Error members::
|
||||
|
||||
XML_ERROR_ENTITY_INDIRECTIONS
|
||||
XML_ERROR_ENTITY_EXPANSION
|
||||
|
||||
new API functions::
|
||||
|
||||
int XML_GetFeature(XML_Parser parser,
|
||||
enum XML_FeatureEnum feature,
|
||||
long *value);
|
||||
int XML_SetFeature(XML_Parser parser,
|
||||
enum XML_FeatureEnum feature,
|
||||
long value);
|
||||
int XML_GetFeatureDefault(enum XML_FeatureEnum feature,
|
||||
long *value);
|
||||
int XML_SetFeatureDefault(enum XML_FeatureEnum feature,
|
||||
long value);
|
||||
|
||||
XML_FEATURE_MAX_ENTITY_INDIRECTIONS
|
||||
Limit the amount of indirections that are allowed to occur during the
|
||||
expansion of a nested entity. A counter starts when an entity reference
|
||||
is encountered. It resets after the entity is fully expanded. The limit
|
||||
protects the parser against exponential entity expansion attacks (aka
|
||||
billion laughs attack). When the limit is exceeded the parser stops and
|
||||
fails with `XML_ERROR_ENTITY_INDIRECTIONS`.
|
||||
A value of 0 disables the protection.
|
||||
|
||||
Supported range
|
||||
0 .. UINT_MAX
|
||||
Default
|
||||
40
|
||||
|
||||
XML_FEATURE_MAX_ENTITY_EXPANSIONS
|
||||
Limit the total length of all entity expansions throughout the entire
|
||||
document. The lengths of all entities are accumulated in a parser variable.
|
||||
The setting protects against quadratic blowup attacks (lots of expansions
|
||||
of a large entity declaration). When the sum of all entities exceeds
|
||||
the limit, the parser stops and fails with `XML_ERROR_ENTITY_EXPANSION`.
|
||||
A value of 0 disables the protection.
|
||||
|
||||
Supported range
|
||||
0 .. UINT_MAX
|
||||
Default
|
||||
8 MiB
|
||||
|
||||
XML_FEATURE_RESET_DTD
|
||||
Reset all DTD information after the <!DOCTYPE> block has been parsed. When
|
||||
the flag is set (default: false) all DTD information after the
|
||||
endDoctypeDeclHandler has been called. The flag can be set inside the
|
||||
endDoctypeDeclHandler. Without DTD information any entity reference in
|
||||
the document body leads to `XML_ERROR_UNDEFINED_ENTITY`.
|
||||
|
||||
Supported range
|
||||
0, 1
|
||||
Default
|
||||
0
|
||||
|
||||
|
||||
How to avoid XML vulnerabilities
|
||||
================================
|
||||
|
||||
Best practices
|
||||
--------------
|
||||
|
||||
* Don't allow DTDs
|
||||
* Don't expand entities
|
||||
* Don't resolve externals
|
||||
* Limit parse depth
|
||||
* Limit total input size
|
||||
* Limit parse time
|
||||
* Favor a SAX or iterparse-like parser for potential large data
|
||||
* Validate and properly quote arguments to XSL transformations and
|
||||
XPath queries
|
||||
* Don't use XPath expression from untrusted sources
|
||||
* Don't apply XSL transformations that come untrusted sources
|
||||
|
||||
(based on Brad Hill's `Attacking XML Security`_)
|
||||
|
||||
|
||||
Other things to consider
|
||||
========================
|
||||
|
||||
XML, XML parsers and processing libraries have more features and possible
|
||||
issue that could lead to DoS vulnerabilities or security exploits in
|
||||
applications. I have compiled an incomplete list of theoretical issues that
|
||||
need further research and more attention. The list is deliberately pessimistic
|
||||
and a bit paranoid, too. It contains things that might go wrong under daffy
|
||||
circumstances.
|
||||
|
||||
|
||||
attribute blowup / hash collision attack
|
||||
----------------------------------------
|
||||
|
||||
XML parsers may use an algorithm with quadratic runtime O(n :sup:`2`) to
|
||||
handle attributes and namespaces. If it uses hash tables (dictionaries) to
|
||||
store attributes and namespaces the implementation may be vulnerable to
|
||||
hash collision attacks, thus reducing the performance to O(n :sup:`2`) again.
|
||||
In either case an attacker is able to forge a denial of service attack with
|
||||
an XML document that contains thousands upon thousands of attributes in
|
||||
a single node.
|
||||
|
||||
I haven't researched yet if expat, pyexpat or libxml2 are vulnerable.
|
||||
|
||||
|
||||
decompression bomb
|
||||
------------------
|
||||
|
||||
The issue of decompression bombs (aka `ZIP bomb`_) apply to all XML libraries
|
||||
that can parse compressed XML stream like gzipped HTTP streams or LZMA-ed
|
||||
files. For an attacker it can reduce the amount of transmitted data by three
|
||||
magnitudes or more. Gzip is able to compress 1 GiB zeros to roughly 1 MB,
|
||||
lzma is even better::
|
||||
|
||||
$ dd if=/dev/zero bs=1M count=1024 | gzip > zeros.gz
|
||||
$ dd if=/dev/zero bs=1M count=1024 | lzma -z > zeros.xy
|
||||
$ ls -sh zeros.*
|
||||
1020K zeros.gz
|
||||
148K zeros.xy
|
||||
|
||||
None of Python's standard XML libraries decompress streams except for
|
||||
``xmlrpclib``. The module is vulnerable <http://bugs.python.org/issue16043>
|
||||
to decompression bombs.
|
||||
|
||||
lxml can load and process compressed data through libxml2 transparently.
|
||||
libxml2 can handle even very large blobs of compressed data efficiently
|
||||
without using too much memory. But it doesn't protect applications from
|
||||
decompression bombs. A carefully written SAX or iterparse-like approach can
|
||||
be safe.
|
||||
|
||||
|
||||
Processing Instruction
|
||||
----------------------
|
||||
|
||||
`PI`_'s like::
|
||||
|
||||
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
|
||||
|
||||
may impose more threats for XML processing. It depends if and how a
|
||||
processor handles processing instructions. The issue of URL retrieval with
|
||||
network or local file access apply to processing instructions, too.
|
||||
|
||||
|
||||
Other DTD features
|
||||
------------------
|
||||
|
||||
`DTD`_ has more features like ``<!NOTATION>``. I haven't researched how
|
||||
these features may be a security threat.
|
||||
|
||||
|
||||
XPath
|
||||
-----
|
||||
|
||||
XPath statements may introduce DoS vulnerabilities. Code should never execute
|
||||
queries from untrusted sources. An attacker may also be able to create a XML
|
||||
document that makes certain XPath queries costly or resource hungry.
|
||||
|
||||
|
||||
XPath injection attacks
|
||||
-----------------------
|
||||
|
||||
XPath injeciton attacks pretty much work like SQL injection attacks.
|
||||
Arguments to XPath queries must be quoted and validated properly, especially
|
||||
when they are taken from the user. The page `Avoid the dangers of XPath injection`_
|
||||
list some ramifications of XPath injections.
|
||||
|
||||
Python's standard library doesn't have XPath support. Lxml supports
|
||||
parameterized XPath queries which does proper quoting. You just have to use
|
||||
its xpath() method correctly::
|
||||
|
||||
# DON'T
|
||||
>>> tree.xpath("/tag[@id='%s']" % value)
|
||||
|
||||
# instead do
|
||||
>>> tree.xpath("/tag[@id=$tagid]", tagid=name)
|
||||
|
||||
|
||||
XInclude
|
||||
--------
|
||||
|
||||
`XML Inclusion`_ is another way to load and include external files::
|
||||
|
||||
<root xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
<xi:include href="filename.txt" parse="text" />
|
||||
</root>
|
||||
|
||||
This feature should be disabled when XML files from an untrusted source are
|
||||
processed. Some Python XML libraries and libxml2 support XInclude but don't
|
||||
have an option to sandbox inclusion and limit it to allowed directories.
|
||||
|
||||
|
||||
XMLSchema location
|
||||
------------------
|
||||
|
||||
A validating XML parser may download schema files from the information in a
|
||||
``xsi:schemaLocation`` attribute.
|
||||
|
||||
::
|
||||
|
||||
<ead xmlns="urn:isbn:1-931666-22-9"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="urn:isbn:1-931666-22-9 http://www.loc.gov/ead/ead.xsd">
|
||||
</ead>
|
||||
|
||||
|
||||
XSL Transformation
|
||||
------------------
|
||||
|
||||
You should keep in mind that XSLT is a Turing complete language. Never
|
||||
process XSLT code from unknown or untrusted source! XSLT processors may
|
||||
allow you to interact with external resources in ways you can't even imagine.
|
||||
Some processors even support extensions that allow read/write access to file
|
||||
system, access to JRE objects or scripting with Jython.
|
||||
|
||||
Example from `Attacking XML Security`_ for Xalan-J::
|
||||
|
||||
<xsl:stylesheet version="1.0"
|
||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime"
|
||||
xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object"
|
||||
exclude-result-prefixes= "rt ob">
|
||||
<xsl:template match="/">
|
||||
<xsl:variable name="runtimeObject" select="rt:getRuntime()"/>
|
||||
<xsl:variable name="command"
|
||||
select="rt:exec($runtimeObject, 'c:\Windows\system32\cmd.exe')"/>
|
||||
<xsl:variable name="commandAsString" select="ob:toString($command)"/>
|
||||
<xsl:value-of select="$commandAsString"/>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
|
||||
|
||||
Related CVEs
|
||||
============
|
||||
|
||||
CVE-2013-1664
|
||||
Unrestricted entity expansion induces DoS vulnerabilities in Python XML
|
||||
libraries (XML bomb)
|
||||
|
||||
CVE-2013-1665
|
||||
External entity expansion in Python XML libraries inflicts potential
|
||||
security flaws and DoS vulnerabilities
|
||||
|
||||
|
||||
Other languages / frameworks
|
||||
=============================
|
||||
|
||||
Several other programming languages and frameworks are vulnerable as well. A
|
||||
couple of them are affected by the fact that libxml2 up to 2.9.0 has no
|
||||
protection against quadratic blowup attacks. Most of them have potential
|
||||
dangerous default settings for entity expansion and external entities, too.
|
||||
|
||||
Perl
|
||||
----
|
||||
|
||||
Perl's XML::Simple is vulnerable to quadratic entity expansion and external
|
||||
entity expansion (both local and remote).
|
||||
|
||||
|
||||
Ruby
|
||||
----
|
||||
|
||||
Ruby's REXML document parser is vulnerable to entity expansion attacks
|
||||
(both quadratic and exponential) but it doesn't do external entity
|
||||
expansion by default. In order to counteract entity expansion you have to
|
||||
disable the feature::
|
||||
|
||||
REXML::Document.entity_expansion_limit = 0
|
||||
|
||||
libxml-ruby and hpricot don't expand entities in their default configuration.
|
||||
|
||||
|
||||
PHP
|
||||
---
|
||||
|
||||
PHP's SimpleXML API is vulnerable to quadratic entity expansion and loads
|
||||
entites from local and remote resources. The option ``LIBXML_NONET`` disables
|
||||
network access but still allows local file access. ``LIBXML_NOENT`` seems to
|
||||
have no effect on entity expansion in PHP 5.4.6.
|
||||
|
||||
|
||||
C# / .NET / Mono
|
||||
----------------
|
||||
|
||||
Information in `XML DoS and Defenses (MSDN)`_ suggest that .NET is
|
||||
vulnerable with its default settings. The article contains code snippets
|
||||
how to create a secure XML reader::
|
||||
|
||||
XmlReaderSettings settings = new XmlReaderSettings();
|
||||
settings.ProhibitDtd = false;
|
||||
settings.MaxCharactersFromEntities = 1024;
|
||||
settings.XmlResolver = null;
|
||||
XmlReader reader = XmlReader.Create(stream, settings);
|
||||
|
||||
|
||||
Java
|
||||
----
|
||||
|
||||
Untested. The documentation of Xerces and its `Xerces SecurityMananger`_
|
||||
sounds like Xerces is also vulnerable to billion laugh attacks with its
|
||||
default settings. It also does entity resolving when an
|
||||
``org.xml.sax.EntityResolver`` is configured. I'm not yet sure about the
|
||||
default setting here.
|
||||
|
||||
Java specialists suggest to have a custom builder factory::
|
||||
|
||||
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
|
||||
builderFactory.setXIncludeAware(False);
|
||||
builderFactory.setExpandEntityReferences(False);
|
||||
builderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, True);
|
||||
# either
|
||||
builderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", True);
|
||||
# or if you need DTDs
|
||||
builderFactory.setFeature("http://xml.org/sax/features/external-general-entities", False);
|
||||
builderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", False);
|
||||
builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", False);
|
||||
builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", False);
|
||||
|
||||
|
||||
TODO
|
||||
====
|
||||
|
||||
* DOM: Use xml.dom.xmlbuilder options for entity handling
|
||||
* SAX: take feature_external_ges and feature_external_pes (?) into account
|
||||
* test experimental monkey patching of stdlib modules
|
||||
* improve documentation
|
||||
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Copyright (c) 2013-2017 by Christian Heimes <christian@python.org>
|
||||
|
||||
Licensed to PSF under a Contributor Agreement.
|
||||
|
||||
See http://www.python.org/psf/license for licensing details.
|
||||
|
||||
|
||||
Acknowledgements
|
||||
================
|
||||
|
||||
Brett Cannon (Python Core developer)
|
||||
review and code cleanup
|
||||
|
||||
Antoine Pitrou (Python Core developer)
|
||||
code review
|
||||
|
||||
Aaron Patterson, Ben Murphy and Michael Koziarski (Ruby community)
|
||||
Many thanks to Aaron, Ben and Michael from the Ruby community for their
|
||||
report and assistance.
|
||||
|
||||
Thierry Carrez (OpenStack)
|
||||
Many thanks to Thierry for his report to the Python Security Response
|
||||
Team on behalf of the OpenStack security team.
|
||||
|
||||
Carl Meyer (Django)
|
||||
Many thanks to Carl for his report to PSRT on behalf of the Django security
|
||||
team.
|
||||
|
||||
Daniel Veillard (libxml2)
|
||||
Many thanks to Daniel for his insight and assistance with libxml2.
|
||||
|
||||
semantics GmbH (http://www.semantics.de/)
|
||||
Many thanks to my employer semantics for letting me work on the issue
|
||||
during working hours as part of semantics's open source initiative.
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
* `XML DoS and Defenses (MSDN)`_
|
||||
* `Billion Laughs`_ on Wikipedia
|
||||
* `ZIP bomb`_ on Wikipedia
|
||||
* `Configure SAX parsers for secure processing`_
|
||||
* `Testing for XML Injection`_
|
||||
|
||||
.. _defusedxml package: https://bitbucket.org/tiran/defusedxml
|
||||
.. _defusedxml on PyPI: https://pypi.python.org/pypi/defusedxml
|
||||
.. _defusedexpat package: https://bitbucket.org/tiran/defusedexpat
|
||||
.. _defusedexpat on PyPI: https://pypi.python.org/pypi/defusedexpat
|
||||
.. _modified expat: https://bitbucket.org/tiran/expat
|
||||
.. _expat parser: http://expat.sourceforge.net/
|
||||
.. _Attacking XML Security: https://www.isecpartners.com/media/12976/iSEC-HILL-Attacking-XML-Security-bh07.pdf
|
||||
.. _Billion Laughs: http://en.wikipedia.org/wiki/Billion_laughs
|
||||
.. _XML DoS and Defenses (MSDN): http://msdn.microsoft.com/en-us/magazine/ee335713.aspx
|
||||
.. _ZIP bomb: http://en.wikipedia.org/wiki/Zip_bomb
|
||||
.. _DTD: http://en.wikipedia.org/wiki/Document_Type_Definition
|
||||
.. _PI: https://en.wikipedia.org/wiki/Processing_Instruction
|
||||
.. _Avoid the dangers of XPath injection: http://www.ibm.com/developerworks/xml/library/x-xpathinjection/index.html
|
||||
.. _Configure SAX parsers for secure processing: http://www.ibm.com/developerworks/xml/library/x-tipcfsx/index.html
|
||||
.. _Testing for XML Injection: https://www.owasp.org/index.php/Testing_for_XML_Injection_(OWASP-DV-008)
|
||||
.. _Xerces SecurityMananger: http://xerces.apache.org/xerces2-j/javadocs/xerces2/org/apache/xerces/util/SecurityManager.html
|
||||
.. _XML Inclusion: http://www.w3.org/TR/xinclude/#include_element
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
defusedxml 0.5.0
|
||||
----------------
|
||||
|
||||
*Release date: 07-Feb-2017*
|
||||
|
||||
- No changes
|
||||
|
||||
defusedxml 0.5.0.rc1
|
||||
--------------------
|
||||
|
||||
*Release date: 28-Jan-2017*
|
||||
|
||||
- Add compatibility with Python 3.6
|
||||
- Drop support for Python 2.6, 3.1, 3.2, 3.3
|
||||
- Fix lxml tests (XMLSyntaxError: Detected an entity reference loop)
|
||||
|
||||
|
||||
defusedxml 0.4.1
|
||||
----------------
|
||||
|
||||
*Release date: 28-Mar-2013*
|
||||
|
||||
- Add more demo exploits, e.g. python_external.py and Xalan XSLT demos.
|
||||
- Improved documentation.
|
||||
|
||||
|
||||
defusedxml 0.4
|
||||
--------------
|
||||
|
||||
*Release date: 25-Feb-2013*
|
||||
|
||||
- As per http://seclists.org/oss-sec/2013/q1/340 please REJECT
|
||||
CVE-2013-0278, CVE-2013-0279 and CVE-2013-0280 and use CVE-2013-1664,
|
||||
CVE-2013-1665 for OpenStack/etc.
|
||||
- Add missing parser_list argument to sax.make_parser(). The argument is
|
||||
ignored, though. (thanks to Florian Apolloner)
|
||||
- Add demo exploit for external entity attack on Python's SAX parser, XML-RPC
|
||||
and WebDAV.
|
||||
|
||||
|
||||
defusedxml 0.3
|
||||
--------------
|
||||
|
||||
*Release date: 19-Feb-2013*
|
||||
|
||||
- Improve documentation
|
||||
|
||||
|
||||
defusedxml 0.2
|
||||
--------------
|
||||
|
||||
*Release date: 15-Feb-2013*
|
||||
|
||||
- Rename ExternalEntitiesForbidden to ExternalReferenceForbidden
|
||||
- Rename defusedxml.lxml.check_dtd() to check_docinfo()
|
||||
- Unify argument names in callbacks
|
||||
- Add arguments and formatted representation to exceptions
|
||||
- Add forbid_external argument to all functions and classs
|
||||
- More tests
|
||||
- LOTS of documentation
|
||||
- Add example code for other languages (Ruby, Perl, PHP) and parsers (Genshi)
|
||||
- Add protection against XML and gzip attacks to xmlrpclib
|
||||
|
||||
defusedxml 0.1
|
||||
--------------
|
||||
|
||||
*Release date: 08-Feb-2013*
|
||||
|
||||
- Initial and internal release for PSRT review
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
defusedxml/ElementTree.py,sha256=2U5ZI_-Hyaxa6ExNSos7PmXWWPKGDywM4TfdXfYp4Oc,3766
|
||||
defusedxml/__init__.py,sha256=C167JKHHezQZccLSaQWBvWLdLSLzgyGGYP6gk-6C6pI,1317
|
||||
defusedxml/cElementTree.py,sha256=UX_zROjHHbK_p0zgqrVYrMKJaS-S3JdV5Sb9flsjhrM,1039
|
||||
defusedxml/common.py,sha256=tDVeUaPPXwz1uGK6KywJXbFVIHOTwSRt4Ff8xCmzKog,4016
|
||||
defusedxml/expatbuilder.py,sha256=UuTvbviQ2V4BrjzK0B7IVO_3fFlzyM1vOCR8F2RZ-eY,3990
|
||||
defusedxml/expatreader.py,sha256=M2KdCKQug6i60qCr433epQo0bvqKHiO7l5CtN7pO74M,2307
|
||||
defusedxml/lxml.py,sha256=P8yxaANK5W7vtqrbQTU6CkF4A2-Lk8HbuG9hjbX62mQ,4976
|
||||
defusedxml/minidom.py,sha256=W2upditczs87W7MLTeW5k4rTmPEgTVipLxOvqpHCpKg,1865
|
||||
defusedxml/pulldom.py,sha256=L2e7wXKeo9QEJo2nRVT6DyNZzhGX6b49hH3Z3PfuQbw,1162
|
||||
defusedxml/sax.py,sha256=xEumsdgS5TZbpYz4qFuS2U26MR9yL6W7nKsX6EyrmCE,1464
|
||||
defusedxml/xmlrpc.py,sha256=68hIIn3edVOdYoO7URab8N7_HazFu1Ot__P6zCg2MEI,5417
|
||||
defusedxml-0.5.0.dist-info/DESCRIPTION.rst,sha256=4DYB6L167IcLWRB6x4HcSsklMhz5eXGwniejibFFA28,28590
|
||||
defusedxml-0.5.0.dist-info/METADATA,sha256=hMi15SBMg2B6FM_0VJMBk-dbCP1g390AOHsNUmLNIDg,29519
|
||||
defusedxml-0.5.0.dist-info/RECORD,,
|
||||
defusedxml-0.5.0.dist-info/WHEEL,sha256=5wvfB7GvgZAbKBSE9uX9Zbi6LCL-_KgezgHblXhCRnM,113
|
||||
defusedxml-0.5.0.dist-info/metadata.json,sha256=haTsjz9D53YTHsd8eJyFvbFl-HXS-2KKTTOO7OlZpoM,1068
|
||||
defusedxml-0.5.0.dist-info/top_level.txt,sha256=QGHa90F50pVKhWSFlERI0jtSKtqDiGyfeZX7dQNZAAw,11
|
||||
defusedxml-0.5.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
defusedxml/__pycache__/expatbuilder.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/ElementTree.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/lxml.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/sax.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/cElementTree.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/common.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/xmlrpc.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/expatreader.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/pulldom.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/minidom.cpython-36.pyc,,
|
||||
defusedxml/__pycache__/__init__.cpython-36.pyc,,
|
||||
@@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.30.0.a0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Python Software Foundation License", "Natural Language :: English", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Text Processing :: Markup :: XML"], "download_url": "https://pypi.python.org/pypi/defusedxml", "extensions": {"python.details": {"contacts": [{"email": "christian@python.org", "name": "Christian Heimes", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/tiran/defusedxml"}}}, "generator": "bdist_wheel (0.30.0.a0)", "keywords": ["xml", "bomb", "DoS"], "license": "PSFL", "metadata_version": "2.0", "name": "defusedxml", "platform": "all", "summary": "XML bomb protection for Python stdlib modules", "version": "0.5.0"}
|
||||
@@ -0,0 +1 @@
|
||||
defusedxml
|
||||
@@ -0,0 +1,112 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.etree.ElementTree facade
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import sys
|
||||
from xml.etree.ElementTree import TreeBuilder as _TreeBuilder
|
||||
from xml.etree.ElementTree import parse as _parse
|
||||
from xml.etree.ElementTree import tostring
|
||||
|
||||
from .common import PY3
|
||||
|
||||
|
||||
if PY3:
|
||||
import importlib
|
||||
else:
|
||||
from xml.etree.ElementTree import XMLParser as _XMLParser
|
||||
from xml.etree.ElementTree import iterparse as _iterparse
|
||||
from xml.etree.ElementTree import ParseError
|
||||
|
||||
|
||||
from .common import (DTDForbidden, EntitiesForbidden,
|
||||
ExternalReferenceForbidden, _generate_etree_functions)
|
||||
|
||||
__origin__ = "xml.etree.ElementTree"
|
||||
|
||||
|
||||
def _get_py3_cls():
|
||||
"""Python 3.3 hides the pure Python code but defusedxml requires it.
|
||||
|
||||
The code is based on test.support.import_fresh_module().
|
||||
"""
|
||||
pymodname = "xml.etree.ElementTree"
|
||||
cmodname = "_elementtree"
|
||||
|
||||
pymod = sys.modules.pop(pymodname, None)
|
||||
cmod = sys.modules.pop(cmodname, None)
|
||||
|
||||
sys.modules[cmodname] = None
|
||||
pure_pymod = importlib.import_module(pymodname)
|
||||
if cmod is not None:
|
||||
sys.modules[cmodname] = cmod
|
||||
else:
|
||||
sys.modules.pop(cmodname)
|
||||
sys.modules[pymodname] = pymod
|
||||
|
||||
_XMLParser = pure_pymod.XMLParser
|
||||
_iterparse = pure_pymod.iterparse
|
||||
ParseError = pure_pymod.ParseError
|
||||
|
||||
return _XMLParser, _iterparse, ParseError
|
||||
|
||||
|
||||
if PY3:
|
||||
_XMLParser, _iterparse, ParseError = _get_py3_cls()
|
||||
|
||||
|
||||
class DefusedXMLParser(_XMLParser):
|
||||
|
||||
def __init__(self, html=0, target=None, encoding=None,
|
||||
forbid_dtd=False, forbid_entities=True,
|
||||
forbid_external=True):
|
||||
# Python 2.x old style class
|
||||
_XMLParser.__init__(self, html, target, encoding)
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
self.forbid_external = forbid_external
|
||||
if PY3:
|
||||
parser = self.parser
|
||||
else:
|
||||
parser = self._parser
|
||||
if self.forbid_dtd:
|
||||
parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl
|
||||
if self.forbid_entities:
|
||||
parser.EntityDeclHandler = self.defused_entity_decl
|
||||
parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl
|
||||
if self.forbid_external:
|
||||
parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler
|
||||
|
||||
def defused_start_doctype_decl(self, name, sysid, pubid,
|
||||
has_internal_subset):
|
||||
raise DTDForbidden(name, sysid, pubid)
|
||||
|
||||
def defused_entity_decl(self, name, is_parameter_entity, value, base,
|
||||
sysid, pubid, notation_name):
|
||||
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_unparsed_entity_decl(self, name, base, sysid, pubid,
|
||||
notation_name):
|
||||
# expat 1.2
|
||||
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_external_entity_ref_handler(self, context, base, sysid,
|
||||
pubid):
|
||||
raise ExternalReferenceForbidden(context, base, sysid, pubid)
|
||||
|
||||
|
||||
# aliases
|
||||
XMLTreeBuilder = XMLParse = DefusedXMLParser
|
||||
|
||||
parse, iterparse, fromstring = _generate_etree_functions(DefusedXMLParser,
|
||||
_TreeBuilder, _parse,
|
||||
_iterparse)
|
||||
XML = fromstring
|
||||
|
||||
|
||||
__all__ = ['XML', 'XMLParse', 'XMLTreeBuilder', 'fromstring', 'iterparse',
|
||||
'parse', 'tostring']
|
||||
@@ -0,0 +1,45 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defuse XML bomb denial of service vulnerabilities
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from .common import (DefusedXmlException, DTDForbidden, EntitiesForbidden,
|
||||
ExternalReferenceForbidden, NotSupportedError,
|
||||
_apply_defusing)
|
||||
|
||||
|
||||
def defuse_stdlib():
|
||||
"""Monkey patch and defuse all stdlib packages
|
||||
|
||||
:warning: The monkey patch is an EXPERIMETNAL feature.
|
||||
"""
|
||||
defused = {}
|
||||
|
||||
from . import cElementTree
|
||||
from . import ElementTree
|
||||
from . import minidom
|
||||
from . import pulldom
|
||||
from . import sax
|
||||
from . import expatbuilder
|
||||
from . import expatreader
|
||||
from . import xmlrpc
|
||||
|
||||
xmlrpc.monkey_patch()
|
||||
defused[xmlrpc] = None
|
||||
|
||||
for defused_mod in [cElementTree, ElementTree, minidom, pulldom, sax,
|
||||
expatbuilder, expatreader]:
|
||||
stdlib_mod = _apply_defusing(defused_mod)
|
||||
defused[defused_mod] = stdlib_mod
|
||||
|
||||
return defused
|
||||
|
||||
|
||||
__version__ = "0.5.0"
|
||||
|
||||
__all__ = ['DefusedXmlException', 'DTDForbidden', 'EntitiesForbidden',
|
||||
'ExternalReferenceForbidden', 'NotSupportedError']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,30 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.etree.cElementTree
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from xml.etree.cElementTree import TreeBuilder as _TreeBuilder
|
||||
from xml.etree.cElementTree import parse as _parse
|
||||
from xml.etree.cElementTree import tostring
|
||||
# iterparse from ElementTree!
|
||||
from xml.etree.ElementTree import iterparse as _iterparse
|
||||
|
||||
from .ElementTree import DefusedXMLParser
|
||||
from .common import _generate_etree_functions
|
||||
|
||||
__origin__ = "xml.etree.cElementTree"
|
||||
|
||||
|
||||
XMLTreeBuilder = XMLParse = DefusedXMLParser
|
||||
|
||||
parse, iterparse, fromstring = _generate_etree_functions(DefusedXMLParser,
|
||||
_TreeBuilder, _parse,
|
||||
_iterparse)
|
||||
XML = fromstring
|
||||
|
||||
__all__ = ['XML', 'XMLParse', 'XMLTreeBuilder', 'fromstring', 'iterparse',
|
||||
'parse', 'tostring']
|
||||
@@ -0,0 +1,120 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Common constants, exceptions and helpe functions
|
||||
"""
|
||||
import sys
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
|
||||
class DefusedXmlException(ValueError):
|
||||
"""Base exception
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
|
||||
class DTDForbidden(DefusedXmlException):
|
||||
"""Document type definition is forbidden
|
||||
"""
|
||||
|
||||
def __init__(self, name, sysid, pubid):
|
||||
super(DTDForbidden, self).__init__()
|
||||
self.name = name
|
||||
self.sysid = sysid
|
||||
self.pubid = pubid
|
||||
|
||||
def __str__(self):
|
||||
tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})"
|
||||
return tpl.format(self.name, self.sysid, self.pubid)
|
||||
|
||||
|
||||
class EntitiesForbidden(DefusedXmlException):
|
||||
"""Entity definition is forbidden
|
||||
"""
|
||||
|
||||
def __init__(self, name, value, base, sysid, pubid, notation_name):
|
||||
super(EntitiesForbidden, self).__init__()
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.base = base
|
||||
self.sysid = sysid
|
||||
self.pubid = pubid
|
||||
self.notation_name = notation_name
|
||||
|
||||
def __str__(self):
|
||||
tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})"
|
||||
return tpl.format(self.name, self.sysid, self.pubid)
|
||||
|
||||
|
||||
class ExternalReferenceForbidden(DefusedXmlException):
|
||||
"""Resolving an external reference is forbidden
|
||||
"""
|
||||
|
||||
def __init__(self, context, base, sysid, pubid):
|
||||
super(ExternalReferenceForbidden, self).__init__()
|
||||
self.context = context
|
||||
self.base = base
|
||||
self.sysid = sysid
|
||||
self.pubid = pubid
|
||||
|
||||
def __str__(self):
|
||||
tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})"
|
||||
return tpl.format(self.sysid, self.pubid)
|
||||
|
||||
|
||||
class NotSupportedError(DefusedXmlException):
|
||||
"""The operation is not supported
|
||||
"""
|
||||
|
||||
|
||||
def _apply_defusing(defused_mod):
|
||||
assert defused_mod is sys.modules[defused_mod.__name__]
|
||||
stdlib_name = defused_mod.__origin__
|
||||
__import__(stdlib_name, {}, {}, ["*"])
|
||||
stdlib_mod = sys.modules[stdlib_name]
|
||||
stdlib_names = set(dir(stdlib_mod))
|
||||
for name, obj in vars(defused_mod).items():
|
||||
if name.startswith("_") or name not in stdlib_names:
|
||||
continue
|
||||
setattr(stdlib_mod, name, obj)
|
||||
return stdlib_mod
|
||||
|
||||
|
||||
def _generate_etree_functions(DefusedXMLParser, _TreeBuilder,
|
||||
_parse, _iterparse):
|
||||
"""Factory for functions needed by etree, dependent on whether
|
||||
cElementTree or ElementTree is used."""
|
||||
|
||||
def parse(source, parser=None, forbid_dtd=False, forbid_entities=True,
|
||||
forbid_external=True):
|
||||
if parser is None:
|
||||
parser = DefusedXMLParser(target=_TreeBuilder(),
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external)
|
||||
return _parse(source, parser)
|
||||
|
||||
def iterparse(source, events=None, parser=None, forbid_dtd=False,
|
||||
forbid_entities=True, forbid_external=True):
|
||||
if parser is None:
|
||||
parser = DefusedXMLParser(target=_TreeBuilder(),
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external)
|
||||
return _iterparse(source, events, parser)
|
||||
|
||||
def fromstring(text, forbid_dtd=False, forbid_entities=True,
|
||||
forbid_external=True):
|
||||
parser = DefusedXMLParser(target=_TreeBuilder(),
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external)
|
||||
parser.feed(text)
|
||||
return parser.close()
|
||||
|
||||
return parse, iterparse, fromstring
|
||||
@@ -0,0 +1,110 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.dom.expatbuilder
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.dom.expatbuilder import ExpatBuilder as _ExpatBuilder
|
||||
from xml.dom.expatbuilder import Namespaces as _Namespaces
|
||||
|
||||
from .common import (DTDForbidden, EntitiesForbidden,
|
||||
ExternalReferenceForbidden)
|
||||
|
||||
__origin__ = "xml.dom.expatbuilder"
|
||||
|
||||
|
||||
class DefusedExpatBuilder(_ExpatBuilder):
|
||||
"""Defused document builder"""
|
||||
|
||||
def __init__(self, options=None, forbid_dtd=False, forbid_entities=True,
|
||||
forbid_external=True):
|
||||
_ExpatBuilder.__init__(self, options)
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
self.forbid_external = forbid_external
|
||||
|
||||
def defused_start_doctype_decl(self, name, sysid, pubid,
|
||||
has_internal_subset):
|
||||
raise DTDForbidden(name, sysid, pubid)
|
||||
|
||||
def defused_entity_decl(self, name, is_parameter_entity, value, base,
|
||||
sysid, pubid, notation_name):
|
||||
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_unparsed_entity_decl(self, name, base, sysid, pubid,
|
||||
notation_name):
|
||||
# expat 1.2
|
||||
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_external_entity_ref_handler(self, context, base, sysid,
|
||||
pubid):
|
||||
raise ExternalReferenceForbidden(context, base, sysid, pubid)
|
||||
|
||||
def install(self, parser):
|
||||
_ExpatBuilder.install(self, parser)
|
||||
|
||||
if self.forbid_dtd:
|
||||
parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl
|
||||
if self.forbid_entities:
|
||||
# if self._options.entities:
|
||||
parser.EntityDeclHandler = self.defused_entity_decl
|
||||
parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl
|
||||
if self.forbid_external:
|
||||
parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler
|
||||
|
||||
|
||||
class DefusedExpatBuilderNS(_Namespaces, DefusedExpatBuilder):
|
||||
"""Defused document builder that supports namespaces."""
|
||||
|
||||
def install(self, parser):
|
||||
DefusedExpatBuilder.install(self, parser)
|
||||
if self._options.namespace_declarations:
|
||||
parser.StartNamespaceDeclHandler = (
|
||||
self.start_namespace_decl_handler)
|
||||
|
||||
def reset(self):
|
||||
DefusedExpatBuilder.reset(self)
|
||||
self._initNamespaces()
|
||||
|
||||
|
||||
def parse(file, namespaces=True, forbid_dtd=False, forbid_entities=True,
|
||||
forbid_external=True):
|
||||
"""Parse a document, returning the resulting Document node.
|
||||
|
||||
'file' may be either a file name or an open file object.
|
||||
"""
|
||||
if namespaces:
|
||||
build_builder = DefusedExpatBuilderNS
|
||||
else:
|
||||
build_builder = DefusedExpatBuilder
|
||||
builder = build_builder(forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external)
|
||||
|
||||
if isinstance(file, str):
|
||||
fp = open(file, 'rb')
|
||||
try:
|
||||
result = builder.parseFile(fp)
|
||||
finally:
|
||||
fp.close()
|
||||
else:
|
||||
result = builder.parseFile(file)
|
||||
return result
|
||||
|
||||
|
||||
def parseString(string, namespaces=True, forbid_dtd=False,
|
||||
forbid_entities=True, forbid_external=True):
|
||||
"""Parse a document from a string, returning the resulting
|
||||
Document node.
|
||||
"""
|
||||
if namespaces:
|
||||
build_builder = DefusedExpatBuilderNS
|
||||
else:
|
||||
build_builder = DefusedExpatBuilder
|
||||
builder = build_builder(forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external)
|
||||
return builder.parseString(string)
|
||||
@@ -0,0 +1,59 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.sax.expatreader
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.sax.expatreader import ExpatParser as _ExpatParser
|
||||
|
||||
from .common import (DTDForbidden, EntitiesForbidden,
|
||||
ExternalReferenceForbidden)
|
||||
|
||||
__origin__ = "xml.sax.expatreader"
|
||||
|
||||
|
||||
class DefusedExpatParser(_ExpatParser):
|
||||
"""Defused SAX driver for the pyexpat C module."""
|
||||
|
||||
def __init__(self, namespaceHandling=0, bufsize=2 ** 16 - 20,
|
||||
forbid_dtd=False, forbid_entities=True,
|
||||
forbid_external=True):
|
||||
_ExpatParser.__init__(self, namespaceHandling, bufsize)
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
self.forbid_external = forbid_external
|
||||
|
||||
def defused_start_doctype_decl(self, name, sysid, pubid,
|
||||
has_internal_subset):
|
||||
raise DTDForbidden(name, sysid, pubid)
|
||||
|
||||
def defused_entity_decl(self, name, is_parameter_entity, value, base,
|
||||
sysid, pubid, notation_name):
|
||||
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_unparsed_entity_decl(self, name, base, sysid, pubid,
|
||||
notation_name):
|
||||
# expat 1.2
|
||||
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_external_entity_ref_handler(self, context, base, sysid,
|
||||
pubid):
|
||||
raise ExternalReferenceForbidden(context, base, sysid, pubid)
|
||||
|
||||
def reset(self):
|
||||
_ExpatParser.reset(self)
|
||||
parser = self._parser
|
||||
if self.forbid_dtd:
|
||||
parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl
|
||||
if self.forbid_entities:
|
||||
parser.EntityDeclHandler = self.defused_entity_decl
|
||||
parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl
|
||||
if self.forbid_external:
|
||||
parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler
|
||||
|
||||
|
||||
def create_parser(*args, **kwargs):
|
||||
return DefusedExpatParser(*args, **kwargs)
|
||||
@@ -0,0 +1,153 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Example code for lxml.etree protection
|
||||
|
||||
The code has NO protection against decompression bombs.
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import threading
|
||||
from lxml import etree as _etree
|
||||
|
||||
from .common import DTDForbidden, EntitiesForbidden, NotSupportedError
|
||||
|
||||
LXML3 = _etree.LXML_VERSION[0] >= 3
|
||||
|
||||
__origin__ = "lxml.etree"
|
||||
|
||||
tostring = _etree.tostring
|
||||
|
||||
|
||||
class RestrictedElement(_etree.ElementBase):
|
||||
"""A restricted Element class that filters out instances of some classes
|
||||
"""
|
||||
__slots__ = ()
|
||||
# blacklist = (etree._Entity, etree._ProcessingInstruction, etree._Comment)
|
||||
blacklist = _etree._Entity
|
||||
|
||||
def _filter(self, iterator):
|
||||
blacklist = self.blacklist
|
||||
for child in iterator:
|
||||
if isinstance(child, blacklist):
|
||||
continue
|
||||
yield child
|
||||
|
||||
def __iter__(self):
|
||||
iterator = super(RestrictedElement, self).__iter__()
|
||||
return self._filter(iterator)
|
||||
|
||||
def iterchildren(self, tag=None, reversed=False):
|
||||
iterator = super(RestrictedElement, self).iterchildren(
|
||||
tag=tag, reversed=reversed)
|
||||
return self._filter(iterator)
|
||||
|
||||
def iter(self, tag=None, *tags):
|
||||
iterator = super(RestrictedElement, self).iter(tag=tag, *tags)
|
||||
return self._filter(iterator)
|
||||
|
||||
def iterdescendants(self, tag=None, *tags):
|
||||
iterator = super(RestrictedElement,
|
||||
self).iterdescendants(tag=tag, *tags)
|
||||
return self._filter(iterator)
|
||||
|
||||
def itersiblings(self, tag=None, preceding=False):
|
||||
iterator = super(RestrictedElement, self).itersiblings(
|
||||
tag=tag, preceding=preceding)
|
||||
return self._filter(iterator)
|
||||
|
||||
def getchildren(self):
|
||||
iterator = super(RestrictedElement, self).__iter__()
|
||||
return list(self._filter(iterator))
|
||||
|
||||
def getiterator(self, tag=None):
|
||||
iterator = super(RestrictedElement, self).getiterator(tag)
|
||||
return self._filter(iterator)
|
||||
|
||||
|
||||
class GlobalParserTLS(threading.local):
|
||||
"""Thread local context for custom parser instances
|
||||
"""
|
||||
parser_config = {
|
||||
'resolve_entities': False,
|
||||
# 'remove_comments': True,
|
||||
# 'remove_pis': True,
|
||||
}
|
||||
|
||||
element_class = RestrictedElement
|
||||
|
||||
def createDefaultParser(self):
|
||||
parser = _etree.XMLParser(**self.parser_config)
|
||||
element_class = self.element_class
|
||||
if self.element_class is not None:
|
||||
lookup = _etree.ElementDefaultClassLookup(element=element_class)
|
||||
parser.set_element_class_lookup(lookup)
|
||||
return parser
|
||||
|
||||
def setDefaultParser(self, parser):
|
||||
self._default_parser = parser
|
||||
|
||||
def getDefaultParser(self):
|
||||
parser = getattr(self, "_default_parser", None)
|
||||
if parser is None:
|
||||
parser = self.createDefaultParser()
|
||||
self.setDefaultParser(parser)
|
||||
return parser
|
||||
|
||||
|
||||
_parser_tls = GlobalParserTLS()
|
||||
getDefaultParser = _parser_tls.getDefaultParser
|
||||
|
||||
|
||||
def check_docinfo(elementtree, forbid_dtd=False, forbid_entities=True):
|
||||
"""Check docinfo of an element tree for DTD and entity declarations
|
||||
|
||||
The check for entity declarations needs lxml 3 or newer. lxml 2.x does
|
||||
not support dtd.iterentities().
|
||||
"""
|
||||
docinfo = elementtree.docinfo
|
||||
if docinfo.doctype:
|
||||
if forbid_dtd:
|
||||
raise DTDForbidden(docinfo.doctype,
|
||||
docinfo.system_url,
|
||||
docinfo.public_id)
|
||||
if forbid_entities and not LXML3:
|
||||
# lxml < 3 has no iterentities()
|
||||
raise NotSupportedError("Unable to check for entity declarations "
|
||||
"in lxml 2.x")
|
||||
|
||||
if forbid_entities:
|
||||
for dtd in docinfo.internalDTD, docinfo.externalDTD:
|
||||
if dtd is None:
|
||||
continue
|
||||
for entity in dtd.iterentities():
|
||||
raise EntitiesForbidden(entity.name, entity.content, None,
|
||||
None, None, None)
|
||||
|
||||
|
||||
def parse(source, parser=None, base_url=None, forbid_dtd=False,
|
||||
forbid_entities=True):
|
||||
if parser is None:
|
||||
parser = getDefaultParser()
|
||||
elementtree = _etree.parse(source, parser, base_url=base_url)
|
||||
check_docinfo(elementtree, forbid_dtd, forbid_entities)
|
||||
return elementtree
|
||||
|
||||
|
||||
def fromstring(text, parser=None, base_url=None, forbid_dtd=False,
|
||||
forbid_entities=True):
|
||||
if parser is None:
|
||||
parser = getDefaultParser()
|
||||
rootelement = _etree.fromstring(text, parser, base_url=base_url)
|
||||
elementtree = rootelement.getroottree()
|
||||
check_docinfo(elementtree, forbid_dtd, forbid_entities)
|
||||
return rootelement
|
||||
|
||||
|
||||
XML = fromstring
|
||||
|
||||
|
||||
def iterparse(*args, **kwargs):
|
||||
raise NotSupportedError("defused lxml.etree.iterparse not available")
|
||||
@@ -0,0 +1,42 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.dom.minidom
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.dom.minidom import _do_pulldom_parse
|
||||
from . import expatbuilder as _expatbuilder
|
||||
from . import pulldom as _pulldom
|
||||
|
||||
__origin__ = "xml.dom.minidom"
|
||||
|
||||
|
||||
def parse(file, parser=None, bufsize=None, forbid_dtd=False,
|
||||
forbid_entities=True, forbid_external=True):
|
||||
"""Parse a file into a DOM by filename or file object."""
|
||||
if parser is None and not bufsize:
|
||||
return _expatbuilder.parse(file, forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external)
|
||||
else:
|
||||
return _do_pulldom_parse(_pulldom.parse, (file,),
|
||||
{'parser': parser, 'bufsize': bufsize,
|
||||
'forbid_dtd': forbid_dtd, 'forbid_entities': forbid_entities,
|
||||
'forbid_external': forbid_external})
|
||||
|
||||
|
||||
def parseString(string, parser=None, forbid_dtd=False,
|
||||
forbid_entities=True, forbid_external=True):
|
||||
"""Parse a file into a DOM from a string."""
|
||||
if parser is None:
|
||||
return _expatbuilder.parseString(string, forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities,
|
||||
forbid_external=forbid_external)
|
||||
else:
|
||||
return _do_pulldom_parse(_pulldom.parseString, (string,),
|
||||
{'parser': parser, 'forbid_dtd': forbid_dtd,
|
||||
'forbid_entities': forbid_entities,
|
||||
'forbid_external': forbid_external})
|
||||
@@ -0,0 +1,34 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.dom.pulldom
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.dom.pulldom import parse as _parse
|
||||
from xml.dom.pulldom import parseString as _parseString
|
||||
from .sax import make_parser
|
||||
|
||||
__origin__ = "xml.dom.pulldom"
|
||||
|
||||
|
||||
def parse(stream_or_string, parser=None, bufsize=None, forbid_dtd=False,
|
||||
forbid_entities=True, forbid_external=True):
|
||||
if parser is None:
|
||||
parser = make_parser()
|
||||
parser.forbid_dtd = forbid_dtd
|
||||
parser.forbid_entities = forbid_entities
|
||||
parser.forbid_external = forbid_external
|
||||
return _parse(stream_or_string, parser, bufsize)
|
||||
|
||||
|
||||
def parseString(string, parser=None, forbid_dtd=False,
|
||||
forbid_entities=True, forbid_external=True):
|
||||
if parser is None:
|
||||
parser = make_parser()
|
||||
parser.forbid_dtd = forbid_dtd
|
||||
parser.forbid_entities = forbid_entities
|
||||
parser.forbid_external = forbid_external
|
||||
return _parseString(string, parser)
|
||||
@@ -0,0 +1,49 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defused xml.sax
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
from xml.sax import InputSource as _InputSource
|
||||
from xml.sax import ErrorHandler as _ErrorHandler
|
||||
|
||||
from . import expatreader
|
||||
|
||||
__origin__ = "xml.sax"
|
||||
|
||||
|
||||
def parse(source, handler, errorHandler=_ErrorHandler(), forbid_dtd=False,
|
||||
forbid_entities=True, forbid_external=True):
|
||||
parser = make_parser()
|
||||
parser.setContentHandler(handler)
|
||||
parser.setErrorHandler(errorHandler)
|
||||
parser.forbid_dtd = forbid_dtd
|
||||
parser.forbid_entities = forbid_entities
|
||||
parser.forbid_external = forbid_external
|
||||
parser.parse(source)
|
||||
|
||||
|
||||
def parseString(string, handler, errorHandler=_ErrorHandler(),
|
||||
forbid_dtd=False, forbid_entities=True,
|
||||
forbid_external=True):
|
||||
from io import BytesIO
|
||||
|
||||
if errorHandler is None:
|
||||
errorHandler = _ErrorHandler()
|
||||
parser = make_parser()
|
||||
parser.setContentHandler(handler)
|
||||
parser.setErrorHandler(errorHandler)
|
||||
parser.forbid_dtd = forbid_dtd
|
||||
parser.forbid_entities = forbid_entities
|
||||
parser.forbid_external = forbid_external
|
||||
|
||||
inpsrc = _InputSource()
|
||||
inpsrc.setByteStream(BytesIO(string))
|
||||
parser.parse(inpsrc)
|
||||
|
||||
|
||||
def make_parser(parser_list=[]):
|
||||
return expatreader.create_parser()
|
||||
@@ -0,0 +1,157 @@
|
||||
# defusedxml
|
||||
#
|
||||
# Copyright (c) 2013 by Christian Heimes <christian@python.org>
|
||||
# Licensed to PSF under a Contributor Agreement.
|
||||
# See http://www.python.org/psf/license for licensing details.
|
||||
"""Defused xmlrpclib
|
||||
|
||||
Also defuses gzip bomb
|
||||
"""
|
||||
from __future__ import print_function, absolute_import
|
||||
|
||||
import io
|
||||
|
||||
from .common import (
|
||||
DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden, PY3)
|
||||
|
||||
if PY3:
|
||||
__origin__ = "xmlrpc.client"
|
||||
from xmlrpc.client import ExpatParser
|
||||
from xmlrpc import client as xmlrpc_client
|
||||
from xmlrpc import server as xmlrpc_server
|
||||
from xmlrpc.client import gzip_decode as _orig_gzip_decode
|
||||
from xmlrpc.client import GzipDecodedResponse as _OrigGzipDecodedResponse
|
||||
else:
|
||||
__origin__ = "xmlrpclib"
|
||||
from xmlrpclib import ExpatParser
|
||||
import xmlrpclib as xmlrpc_client
|
||||
xmlrpc_server = None
|
||||
from xmlrpclib import gzip_decode as _orig_gzip_decode
|
||||
from xmlrpclib import GzipDecodedResponse as _OrigGzipDecodedResponse
|
||||
|
||||
try:
|
||||
import gzip
|
||||
except ImportError:
|
||||
gzip = None
|
||||
|
||||
|
||||
# Limit maximum request size to prevent resource exhaustion DoS
|
||||
# Also used to limit maximum amount of gzip decoded data in order to prevent
|
||||
# decompression bombs
|
||||
# A value of -1 or smaller disables the limit
|
||||
MAX_DATA = 30 * 1024 * 1024 # 30 MB
|
||||
|
||||
|
||||
def defused_gzip_decode(data, limit=None):
|
||||
"""gzip encoded data -> unencoded data
|
||||
|
||||
Decode data using the gzip content encoding as described in RFC 1952
|
||||
"""
|
||||
if not gzip:
|
||||
raise NotImplementedError
|
||||
if limit is None:
|
||||
limit = MAX_DATA
|
||||
f = io.BytesIO(data)
|
||||
gzf = gzip.GzipFile(mode="rb", fileobj=f)
|
||||
try:
|
||||
if limit < 0: # no limit
|
||||
decoded = gzf.read()
|
||||
else:
|
||||
decoded = gzf.read(limit + 1)
|
||||
except IOError:
|
||||
raise ValueError("invalid data")
|
||||
f.close()
|
||||
gzf.close()
|
||||
if limit >= 0 and len(decoded) > limit:
|
||||
raise ValueError("max gzipped payload length exceeded")
|
||||
return decoded
|
||||
|
||||
|
||||
class DefusedGzipDecodedResponse(gzip.GzipFile if gzip else object):
|
||||
"""a file-like object to decode a response encoded with the gzip
|
||||
method, as described in RFC 1952.
|
||||
"""
|
||||
|
||||
def __init__(self, response, limit=None):
|
||||
# response doesn't support tell() and read(), required by
|
||||
# GzipFile
|
||||
if not gzip:
|
||||
raise NotImplementedError
|
||||
self.limit = limit = limit if limit is not None else MAX_DATA
|
||||
if limit < 0: # no limit
|
||||
data = response.read()
|
||||
self.readlength = None
|
||||
else:
|
||||
data = response.read(limit + 1)
|
||||
self.readlength = 0
|
||||
if limit >= 0 and len(data) > limit:
|
||||
raise ValueError("max payload length exceeded")
|
||||
self.stringio = io.BytesIO(data)
|
||||
gzip.GzipFile.__init__(self, mode="rb", fileobj=self.stringio)
|
||||
|
||||
def read(self, n):
|
||||
if self.limit >= 0:
|
||||
left = self.limit - self.readlength
|
||||
n = min(n, left + 1)
|
||||
data = gzip.GzipFile.read(self, n)
|
||||
self.readlength += len(data)
|
||||
if self.readlength > self.limit:
|
||||
raise ValueError("max payload length exceeded")
|
||||
return data
|
||||
else:
|
||||
return gzip.GzipFile.read(self, n)
|
||||
|
||||
def close(self):
|
||||
gzip.GzipFile.close(self)
|
||||
self.stringio.close()
|
||||
|
||||
|
||||
class DefusedExpatParser(ExpatParser):
|
||||
|
||||
def __init__(self, target, forbid_dtd=False, forbid_entities=True,
|
||||
forbid_external=True):
|
||||
ExpatParser.__init__(self, target)
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
self.forbid_external = forbid_external
|
||||
parser = self._parser
|
||||
if self.forbid_dtd:
|
||||
parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl
|
||||
if self.forbid_entities:
|
||||
parser.EntityDeclHandler = self.defused_entity_decl
|
||||
parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl
|
||||
if self.forbid_external:
|
||||
parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler
|
||||
|
||||
def defused_start_doctype_decl(self, name, sysid, pubid,
|
||||
has_internal_subset):
|
||||
raise DTDForbidden(name, sysid, pubid)
|
||||
|
||||
def defused_entity_decl(self, name, is_parameter_entity, value, base,
|
||||
sysid, pubid, notation_name):
|
||||
raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_unparsed_entity_decl(self, name, base, sysid, pubid,
|
||||
notation_name):
|
||||
# expat 1.2
|
||||
raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name)
|
||||
|
||||
def defused_external_entity_ref_handler(self, context, base, sysid,
|
||||
pubid):
|
||||
raise ExternalReferenceForbidden(context, base, sysid, pubid)
|
||||
|
||||
|
||||
def monkey_patch():
|
||||
xmlrpc_client.FastParser = DefusedExpatParser
|
||||
xmlrpc_client.GzipDecodedResponse = DefusedGzipDecodedResponse
|
||||
xmlrpc_client.gzip_decode = defused_gzip_decode
|
||||
if xmlrpc_server:
|
||||
xmlrpc_server.gzip_decode = defused_gzip_decode
|
||||
|
||||
|
||||
def unmonkey_patch():
|
||||
xmlrpc_client.FastParser = None
|
||||
xmlrpc_client.GzipDecodedResponse = _OrigGzipDecodedResponse
|
||||
xmlrpc_client.gzip_decode = _orig_gzip_decode
|
||||
if xmlrpc_server:
|
||||
xmlrpc_server.gzip_decode = _orig_gzip_decode
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa
|
||||
|
||||
"""
|
||||
JSON Web Token implementation
|
||||
|
||||
Minimum implementation based on this spec:
|
||||
http://self-issued.info/docs/draft-jones-json-web-token-01.html
|
||||
"""
|
||||
|
||||
|
||||
__title__ = 'pyjwt'
|
||||
__version__ = '1.5.3'
|
||||
__author__ = 'José Padilla'
|
||||
__license__ = 'MIT'
|
||||
__copyright__ = 'Copyright 2015 José Padilla'
|
||||
|
||||
|
||||
from .api_jwt import (
|
||||
encode, decode, register_algorithm, unregister_algorithm,
|
||||
get_unverified_header, PyJWT
|
||||
)
|
||||
from .api_jws import PyJWS
|
||||
from .exceptions import (
|
||||
InvalidTokenError, DecodeError, InvalidAlgorithmError,
|
||||
InvalidAudienceError, ExpiredSignatureError, ImmatureSignatureError,
|
||||
InvalidIssuedAtError, InvalidIssuerError, ExpiredSignature,
|
||||
InvalidAudience, InvalidIssuer, MissingRequiredClaimError
|
||||
)
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
from . import DecodeError, __version__, decode, encode
|
||||
|
||||
|
||||
def encode_payload(args):
|
||||
# Try to encode
|
||||
if args.key is None:
|
||||
raise ValueError('Key is required when encoding. See --help for usage.')
|
||||
|
||||
# Build payload object to encode
|
||||
payload = {}
|
||||
|
||||
for arg in args.payload:
|
||||
k, v = arg.split('=', 1)
|
||||
|
||||
# exp +offset special case?
|
||||
if k == 'exp' and v[0] == '+' and len(v) > 1:
|
||||
v = str(int(time.time()+int(v[1:])))
|
||||
|
||||
# Cast to integer?
|
||||
if v.isdigit():
|
||||
v = int(v)
|
||||
else:
|
||||
# Cast to float?
|
||||
try:
|
||||
v = float(v)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Cast to true, false, or null?
|
||||
constants = {'true': True, 'false': False, 'null': None}
|
||||
|
||||
if v in constants:
|
||||
v = constants[v]
|
||||
|
||||
payload[k] = v
|
||||
|
||||
token = encode(
|
||||
payload,
|
||||
key=args.key,
|
||||
algorithm=args.algorithm
|
||||
)
|
||||
|
||||
return token.decode('utf-8')
|
||||
|
||||
|
||||
def decode_payload(args):
|
||||
try:
|
||||
if sys.stdin.isatty():
|
||||
token = sys.stdin.read()
|
||||
else:
|
||||
token = args.token
|
||||
|
||||
token = token.encode('utf-8')
|
||||
data = decode(token, key=args.key, verify=args.verify)
|
||||
|
||||
return json.dumps(data)
|
||||
|
||||
except DecodeError as e:
|
||||
raise DecodeError('There was an error decoding the token: %s' % e)
|
||||
|
||||
|
||||
def build_argparser():
|
||||
|
||||
usage = '''
|
||||
Encodes or decodes JSON Web Tokens based on input.
|
||||
|
||||
%(prog)s [options] <command> [options] input
|
||||
|
||||
Decoding examples:
|
||||
|
||||
%(prog)s --key=secret decode json.web.token
|
||||
%(prog)s decode --no-verify json.web.token
|
||||
|
||||
Encoding requires the key option and takes space separated key/value pairs
|
||||
separated by equals (=) as input. Examples:
|
||||
|
||||
%(prog)s --key=secret encode iss=me exp=1302049071
|
||||
%(prog)s --key=secret encode foo=bar exp=+10
|
||||
|
||||
The exp key is special and can take an offset to current Unix time.
|
||||
'''
|
||||
|
||||
arg_parser = argparse.ArgumentParser(
|
||||
prog='pyjwt',
|
||||
usage=usage
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
'-v', '--version',
|
||||
action='version',
|
||||
version='%(prog)s ' + __version__
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
'--key',
|
||||
dest='key',
|
||||
metavar='KEY',
|
||||
default=None,
|
||||
help='set the secret key to sign with'
|
||||
)
|
||||
|
||||
arg_parser.add_argument(
|
||||
'--alg',
|
||||
dest='algorithm',
|
||||
metavar='ALG',
|
||||
default='HS256',
|
||||
help='set crypto algorithm to sign with. default=HS256'
|
||||
)
|
||||
|
||||
subparsers = arg_parser.add_subparsers(
|
||||
title='PyJWT subcommands',
|
||||
description='valid subcommands',
|
||||
help='additional help'
|
||||
)
|
||||
|
||||
# Encode subcommand
|
||||
encode_parser = subparsers.add_parser('encode', help='use to encode a supplied payload')
|
||||
|
||||
payload_help = """Payload to encode. Must be a space separated list of key/value
|
||||
pairs separated by equals (=) sign."""
|
||||
|
||||
encode_parser.add_argument('payload', nargs='+', help=payload_help)
|
||||
encode_parser.set_defaults(func=encode_payload)
|
||||
|
||||
# Decode subcommand
|
||||
decode_parser = subparsers.add_parser('decode', help='use to decode a supplied JSON web token')
|
||||
decode_parser.add_argument('token', help='JSON web token to decode.')
|
||||
|
||||
decode_parser.add_argument(
|
||||
'-n', '--no-verify',
|
||||
action='store_false',
|
||||
dest='verify',
|
||||
default=True,
|
||||
help='ignore signature and claims verification on decode'
|
||||
)
|
||||
|
||||
decode_parser.set_defaults(func=decode_payload)
|
||||
|
||||
return arg_parser
|
||||
|
||||
|
||||
def main():
|
||||
arg_parser = build_argparser()
|
||||
|
||||
try:
|
||||
arguments = arg_parser.parse_args(sys.argv[1:])
|
||||
|
||||
output = arguments.func(arguments)
|
||||
|
||||
print(output)
|
||||
except Exception as e:
|
||||
print('There was an unforseen error: ', e)
|
||||
arg_parser.print_help()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,403 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
|
||||
|
||||
from .compat import constant_time_compare, string_types
|
||||
from .exceptions import InvalidKeyError
|
||||
from .utils import (
|
||||
base64url_decode, base64url_encode, der_to_raw_signature,
|
||||
force_bytes, force_unicode, from_base64url_uint, raw_to_der_signature,
|
||||
to_base64url_uint
|
||||
)
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key, load_pem_public_key, load_ssh_public_key
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import (
|
||||
RSAPrivateKey, RSAPublicKey, RSAPrivateNumbers, RSAPublicNumbers,
|
||||
rsa_recover_prime_factors, rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import (
|
||||
EllipticCurvePrivateKey, EllipticCurvePublicKey
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, padding
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
|
||||
has_crypto = True
|
||||
except ImportError:
|
||||
has_crypto = False
|
||||
|
||||
requires_cryptography = set(['RS256', 'RS384', 'RS512', 'ES256', 'ES384',
|
||||
'ES521', 'ES512', 'PS256', 'PS384', 'PS512'])
|
||||
|
||||
|
||||
def get_default_algorithms():
|
||||
"""
|
||||
Returns the algorithms that are implemented by the library.
|
||||
"""
|
||||
default_algorithms = {
|
||||
'none': NoneAlgorithm(),
|
||||
'HS256': HMACAlgorithm(HMACAlgorithm.SHA256),
|
||||
'HS384': HMACAlgorithm(HMACAlgorithm.SHA384),
|
||||
'HS512': HMACAlgorithm(HMACAlgorithm.SHA512)
|
||||
}
|
||||
|
||||
if has_crypto:
|
||||
default_algorithms.update({
|
||||
'RS256': RSAAlgorithm(RSAAlgorithm.SHA256),
|
||||
'RS384': RSAAlgorithm(RSAAlgorithm.SHA384),
|
||||
'RS512': RSAAlgorithm(RSAAlgorithm.SHA512),
|
||||
'ES256': ECAlgorithm(ECAlgorithm.SHA256),
|
||||
'ES384': ECAlgorithm(ECAlgorithm.SHA384),
|
||||
'ES521': ECAlgorithm(ECAlgorithm.SHA512),
|
||||
'ES512': ECAlgorithm(ECAlgorithm.SHA512), # Backward compat for #219 fix
|
||||
'PS256': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256),
|
||||
'PS384': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384),
|
||||
'PS512': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512)
|
||||
})
|
||||
|
||||
return default_algorithms
|
||||
|
||||
|
||||
class Algorithm(object):
|
||||
"""
|
||||
The interface for an algorithm used to sign and verify tokens.
|
||||
"""
|
||||
def prepare_key(self, key):
|
||||
"""
|
||||
Performs necessary validation and conversions on the key and returns
|
||||
the key value in the proper format for sign() and verify().
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def sign(self, msg, key):
|
||||
"""
|
||||
Returns a digital signature for the specified message
|
||||
using the specified key value.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def verify(self, msg, key, sig):
|
||||
"""
|
||||
Verifies that the specified digital signature is valid
|
||||
for the specified message and key values.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def to_jwk(key_obj):
|
||||
"""
|
||||
Serializes a given RSA key into a JWK
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def from_jwk(jwk):
|
||||
"""
|
||||
Deserializes a given RSA key from JWK back into a PublicKey or PrivateKey object
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NoneAlgorithm(Algorithm):
|
||||
"""
|
||||
Placeholder for use when no signing or verification
|
||||
operations are required.
|
||||
"""
|
||||
def prepare_key(self, key):
|
||||
if key == '':
|
||||
key = None
|
||||
|
||||
if key is not None:
|
||||
raise InvalidKeyError('When alg = "none", key value must be None.')
|
||||
|
||||
return key
|
||||
|
||||
def sign(self, msg, key):
|
||||
return b''
|
||||
|
||||
def verify(self, msg, key, sig):
|
||||
return False
|
||||
|
||||
|
||||
class HMACAlgorithm(Algorithm):
|
||||
"""
|
||||
Performs signing and verification operations using HMAC
|
||||
and the specified hash function.
|
||||
"""
|
||||
SHA256 = hashlib.sha256
|
||||
SHA384 = hashlib.sha384
|
||||
SHA512 = hashlib.sha512
|
||||
|
||||
def __init__(self, hash_alg):
|
||||
self.hash_alg = hash_alg
|
||||
|
||||
def prepare_key(self, key):
|
||||
key = force_bytes(key)
|
||||
|
||||
invalid_strings = [
|
||||
b'-----BEGIN PUBLIC KEY-----',
|
||||
b'-----BEGIN CERTIFICATE-----',
|
||||
b'-----BEGIN RSA PUBLIC KEY-----',
|
||||
b'ssh-rsa'
|
||||
]
|
||||
|
||||
if any([string_value in key for string_value in invalid_strings]):
|
||||
raise InvalidKeyError(
|
||||
'The specified key is an asymmetric key or x509 certificate and'
|
||||
' should not be used as an HMAC secret.')
|
||||
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def to_jwk(key_obj):
|
||||
return json.dumps({
|
||||
'k': force_unicode(base64url_encode(force_bytes(key_obj))),
|
||||
'kty': 'oct'
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def from_jwk(jwk):
|
||||
obj = json.loads(jwk)
|
||||
|
||||
if obj.get('kty') != 'oct':
|
||||
raise InvalidKeyError('Not an HMAC key')
|
||||
|
||||
return base64url_decode(obj['k'])
|
||||
|
||||
def sign(self, msg, key):
|
||||
return hmac.new(key, msg, self.hash_alg).digest()
|
||||
|
||||
def verify(self, msg, key, sig):
|
||||
return constant_time_compare(sig, self.sign(msg, key))
|
||||
|
||||
|
||||
if has_crypto:
|
||||
|
||||
class RSAAlgorithm(Algorithm):
|
||||
"""
|
||||
Performs signing and verification operations using
|
||||
RSASSA-PKCS-v1_5 and the specified hash function.
|
||||
"""
|
||||
SHA256 = hashes.SHA256
|
||||
SHA384 = hashes.SHA384
|
||||
SHA512 = hashes.SHA512
|
||||
|
||||
def __init__(self, hash_alg):
|
||||
self.hash_alg = hash_alg
|
||||
|
||||
def prepare_key(self, key):
|
||||
if isinstance(key, RSAPrivateKey) or \
|
||||
isinstance(key, RSAPublicKey):
|
||||
return key
|
||||
|
||||
if isinstance(key, string_types):
|
||||
key = force_bytes(key)
|
||||
|
||||
try:
|
||||
if key.startswith(b'ssh-rsa'):
|
||||
key = load_ssh_public_key(key, backend=default_backend())
|
||||
else:
|
||||
key = load_pem_private_key(key, password=None, backend=default_backend())
|
||||
except ValueError:
|
||||
key = load_pem_public_key(key, backend=default_backend())
|
||||
else:
|
||||
raise TypeError('Expecting a PEM-formatted key.')
|
||||
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def to_jwk(key_obj):
|
||||
obj = None
|
||||
|
||||
if getattr(key_obj, 'private_numbers', None):
|
||||
# Private key
|
||||
numbers = key_obj.private_numbers()
|
||||
|
||||
obj = {
|
||||
'kty': 'RSA',
|
||||
'key_ops': ['sign'],
|
||||
'n': force_unicode(to_base64url_uint(numbers.public_numbers.n)),
|
||||
'e': force_unicode(to_base64url_uint(numbers.public_numbers.e)),
|
||||
'd': force_unicode(to_base64url_uint(numbers.d)),
|
||||
'p': force_unicode(to_base64url_uint(numbers.p)),
|
||||
'q': force_unicode(to_base64url_uint(numbers.q)),
|
||||
'dp': force_unicode(to_base64url_uint(numbers.dmp1)),
|
||||
'dq': force_unicode(to_base64url_uint(numbers.dmq1)),
|
||||
'qi': force_unicode(to_base64url_uint(numbers.iqmp))
|
||||
}
|
||||
|
||||
elif getattr(key_obj, 'verify', None):
|
||||
# Public key
|
||||
numbers = key_obj.public_numbers()
|
||||
|
||||
obj = {
|
||||
'kty': 'RSA',
|
||||
'key_ops': ['verify'],
|
||||
'n': force_unicode(to_base64url_uint(numbers.n)),
|
||||
'e': force_unicode(to_base64url_uint(numbers.e))
|
||||
}
|
||||
else:
|
||||
raise InvalidKeyError('Not a public or private key')
|
||||
|
||||
return json.dumps(obj)
|
||||
|
||||
@staticmethod
|
||||
def from_jwk(jwk):
|
||||
try:
|
||||
obj = json.loads(jwk)
|
||||
except ValueError:
|
||||
raise InvalidKeyError('Key is not valid JSON')
|
||||
|
||||
if obj.get('kty') != 'RSA':
|
||||
raise InvalidKeyError('Not an RSA key')
|
||||
|
||||
if 'd' in obj and 'e' in obj and 'n' in obj:
|
||||
# Private key
|
||||
if 'oth' in obj:
|
||||
raise InvalidKeyError('Unsupported RSA private key: > 2 primes not supported')
|
||||
|
||||
other_props = ['p', 'q', 'dp', 'dq', 'qi']
|
||||
props_found = [prop in obj for prop in other_props]
|
||||
any_props_found = any(props_found)
|
||||
|
||||
if any_props_found and not all(props_found):
|
||||
raise InvalidKeyError('RSA key must include all parameters if any are present besides d')
|
||||
|
||||
public_numbers = RSAPublicNumbers(
|
||||
from_base64url_uint(obj['e']), from_base64url_uint(obj['n'])
|
||||
)
|
||||
|
||||
if any_props_found:
|
||||
numbers = RSAPrivateNumbers(
|
||||
d=from_base64url_uint(obj['d']),
|
||||
p=from_base64url_uint(obj['p']),
|
||||
q=from_base64url_uint(obj['q']),
|
||||
dmp1=from_base64url_uint(obj['dp']),
|
||||
dmq1=from_base64url_uint(obj['dq']),
|
||||
iqmp=from_base64url_uint(obj['qi']),
|
||||
public_numbers=public_numbers
|
||||
)
|
||||
else:
|
||||
d = from_base64url_uint(obj['d'])
|
||||
p, q = rsa_recover_prime_factors(
|
||||
public_numbers.n, d, public_numbers.e
|
||||
)
|
||||
|
||||
numbers = RSAPrivateNumbers(
|
||||
d=d,
|
||||
p=p,
|
||||
q=q,
|
||||
dmp1=rsa_crt_dmp1(d, p),
|
||||
dmq1=rsa_crt_dmq1(d, q),
|
||||
iqmp=rsa_crt_iqmp(p, q),
|
||||
public_numbers=public_numbers
|
||||
)
|
||||
|
||||
return numbers.private_key(default_backend())
|
||||
elif 'n' in obj and 'e' in obj:
|
||||
# Public key
|
||||
numbers = RSAPublicNumbers(
|
||||
from_base64url_uint(obj['e']), from_base64url_uint(obj['n'])
|
||||
)
|
||||
|
||||
return numbers.public_key(default_backend())
|
||||
else:
|
||||
raise InvalidKeyError('Not a public or private key')
|
||||
|
||||
def sign(self, msg, key):
|
||||
return key.sign(msg, padding.PKCS1v15(), self.hash_alg())
|
||||
|
||||
def verify(self, msg, key, sig):
|
||||
try:
|
||||
key.verify(sig, msg, padding.PKCS1v15(), self.hash_alg())
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
class ECAlgorithm(Algorithm):
|
||||
"""
|
||||
Performs signing and verification operations using
|
||||
ECDSA and the specified hash function
|
||||
"""
|
||||
SHA256 = hashes.SHA256
|
||||
SHA384 = hashes.SHA384
|
||||
SHA512 = hashes.SHA512
|
||||
|
||||
def __init__(self, hash_alg):
|
||||
self.hash_alg = hash_alg
|
||||
|
||||
def prepare_key(self, key):
|
||||
if isinstance(key, EllipticCurvePrivateKey) or \
|
||||
isinstance(key, EllipticCurvePublicKey):
|
||||
return key
|
||||
|
||||
if isinstance(key, string_types):
|
||||
key = force_bytes(key)
|
||||
|
||||
# Attempt to load key. We don't know if it's
|
||||
# a Signing Key or a Verifying Key, so we try
|
||||
# the Verifying Key first.
|
||||
try:
|
||||
if key.startswith(b'ecdsa-sha2-'):
|
||||
key = load_ssh_public_key(key, backend=default_backend())
|
||||
else:
|
||||
key = load_pem_public_key(key, backend=default_backend())
|
||||
except ValueError:
|
||||
key = load_pem_private_key(key, password=None, backend=default_backend())
|
||||
|
||||
else:
|
||||
raise TypeError('Expecting a PEM-formatted key.')
|
||||
|
||||
return key
|
||||
|
||||
def sign(self, msg, key):
|
||||
der_sig = key.sign(msg, ec.ECDSA(self.hash_alg()))
|
||||
|
||||
return der_to_raw_signature(der_sig, key.curve)
|
||||
|
||||
def verify(self, msg, key, sig):
|
||||
try:
|
||||
der_sig = raw_to_der_signature(sig, key.curve)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
try:
|
||||
key.verify(der_sig, msg, ec.ECDSA(self.hash_alg()))
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
|
||||
class RSAPSSAlgorithm(RSAAlgorithm):
|
||||
"""
|
||||
Performs a signature using RSASSA-PSS with MGF1
|
||||
"""
|
||||
|
||||
def sign(self, msg, key):
|
||||
return key.sign(
|
||||
msg,
|
||||
padding.PSS(
|
||||
mgf=padding.MGF1(self.hash_alg()),
|
||||
salt_length=self.hash_alg.digest_size
|
||||
),
|
||||
self.hash_alg()
|
||||
)
|
||||
|
||||
def verify(self, msg, key, sig):
|
||||
try:
|
||||
key.verify(
|
||||
sig,
|
||||
msg,
|
||||
padding.PSS(
|
||||
mgf=padding.MGF1(self.hash_alg()),
|
||||
salt_length=self.hash_alg.digest_size
|
||||
),
|
||||
self.hash_alg()
|
||||
)
|
||||
return True
|
||||
except InvalidSignature:
|
||||
return False
|
||||
@@ -0,0 +1,226 @@
|
||||
import binascii
|
||||
import json
|
||||
import warnings
|
||||
|
||||
from collections import Mapping
|
||||
|
||||
from .algorithms import (
|
||||
Algorithm, get_default_algorithms, has_crypto, requires_cryptography # NOQA
|
||||
)
|
||||
from .compat import binary_type, string_types, text_type
|
||||
from .exceptions import DecodeError, InvalidAlgorithmError, InvalidTokenError
|
||||
from .utils import base64url_decode, base64url_encode, force_bytes, merge_dict
|
||||
|
||||
|
||||
class PyJWS(object):
|
||||
header_typ = 'JWT'
|
||||
|
||||
def __init__(self, algorithms=None, options=None):
|
||||
self._algorithms = get_default_algorithms()
|
||||
self._valid_algs = (set(algorithms) if algorithms is not None
|
||||
else set(self._algorithms))
|
||||
|
||||
# Remove algorithms that aren't on the whitelist
|
||||
for key in list(self._algorithms.keys()):
|
||||
if key not in self._valid_algs:
|
||||
del self._algorithms[key]
|
||||
|
||||
if not options:
|
||||
options = {}
|
||||
|
||||
self.options = merge_dict(self._get_default_options(), options)
|
||||
|
||||
@staticmethod
|
||||
def _get_default_options():
|
||||
return {
|
||||
'verify_signature': True
|
||||
}
|
||||
|
||||
def register_algorithm(self, alg_id, alg_obj):
|
||||
"""
|
||||
Registers a new Algorithm for use when creating and verifying tokens.
|
||||
"""
|
||||
if alg_id in self._algorithms:
|
||||
raise ValueError('Algorithm already has a handler.')
|
||||
|
||||
if not isinstance(alg_obj, Algorithm):
|
||||
raise TypeError('Object is not of type `Algorithm`')
|
||||
|
||||
self._algorithms[alg_id] = alg_obj
|
||||
self._valid_algs.add(alg_id)
|
||||
|
||||
def unregister_algorithm(self, alg_id):
|
||||
"""
|
||||
Unregisters an Algorithm for use when creating and verifying tokens
|
||||
Throws KeyError if algorithm is not registered.
|
||||
"""
|
||||
if alg_id not in self._algorithms:
|
||||
raise KeyError('The specified algorithm could not be removed'
|
||||
' because it is not registered.')
|
||||
|
||||
del self._algorithms[alg_id]
|
||||
self._valid_algs.remove(alg_id)
|
||||
|
||||
def get_algorithms(self):
|
||||
"""
|
||||
Returns a list of supported values for the 'alg' parameter.
|
||||
"""
|
||||
return list(self._valid_algs)
|
||||
|
||||
def encode(self, payload, key, algorithm='HS256', headers=None,
|
||||
json_encoder=None):
|
||||
segments = []
|
||||
|
||||
if algorithm is None:
|
||||
algorithm = 'none'
|
||||
|
||||
if algorithm not in self._valid_algs:
|
||||
pass
|
||||
|
||||
# Header
|
||||
header = {'typ': self.header_typ, 'alg': algorithm}
|
||||
|
||||
if headers:
|
||||
self._validate_headers(headers)
|
||||
header.update(headers)
|
||||
|
||||
json_header = force_bytes(
|
||||
json.dumps(
|
||||
header,
|
||||
separators=(',', ':'),
|
||||
cls=json_encoder
|
||||
)
|
||||
)
|
||||
|
||||
segments.append(base64url_encode(json_header))
|
||||
segments.append(base64url_encode(payload))
|
||||
|
||||
# Segments
|
||||
signing_input = b'.'.join(segments)
|
||||
try:
|
||||
alg_obj = self._algorithms[algorithm]
|
||||
key = alg_obj.prepare_key(key)
|
||||
signature = alg_obj.sign(signing_input, key)
|
||||
|
||||
except KeyError:
|
||||
if not has_crypto and algorithm in requires_cryptography:
|
||||
raise NotImplementedError(
|
||||
"Algorithm '%s' could not be found. Do you have cryptography "
|
||||
"installed?" % algorithm
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError('Algorithm not supported')
|
||||
|
||||
segments.append(base64url_encode(signature))
|
||||
|
||||
return b'.'.join(segments)
|
||||
|
||||
def decode(self, jws, key='', verify=True, algorithms=None, options=None,
|
||||
**kwargs):
|
||||
|
||||
merged_options = merge_dict(self.options, options)
|
||||
verify_signature = merged_options['verify_signature']
|
||||
|
||||
if verify_signature and not algorithms:
|
||||
warnings.warn(
|
||||
'It is strongly recommended that you pass in a ' +
|
||||
'value for the "algorithms" argument when calling decode(). ' +
|
||||
'This argument will be mandatory in a future version.',
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
payload, signing_input, header, signature = self._load(jws)
|
||||
|
||||
if not verify:
|
||||
warnings.warn('The verify parameter is deprecated. '
|
||||
'Please use verify_signature in options instead.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
elif verify_signature:
|
||||
self._verify_signature(payload, signing_input, header, signature,
|
||||
key, algorithms)
|
||||
|
||||
return payload
|
||||
|
||||
def get_unverified_header(self, jwt):
|
||||
"""Returns back the JWT header parameters as a dict()
|
||||
|
||||
Note: The signature is not verified so the header parameters
|
||||
should not be fully trusted until signature verification is complete
|
||||
"""
|
||||
headers = self._load(jwt)[2]
|
||||
self._validate_headers(headers)
|
||||
|
||||
return headers
|
||||
|
||||
def _load(self, jwt):
|
||||
if isinstance(jwt, text_type):
|
||||
jwt = jwt.encode('utf-8')
|
||||
|
||||
if not issubclass(type(jwt), binary_type):
|
||||
raise DecodeError("Invalid token type. Token must be a {0}".format(
|
||||
binary_type))
|
||||
|
||||
try:
|
||||
signing_input, crypto_segment = jwt.rsplit(b'.', 1)
|
||||
header_segment, payload_segment = signing_input.split(b'.', 1)
|
||||
except ValueError:
|
||||
raise DecodeError('Not enough segments')
|
||||
|
||||
try:
|
||||
header_data = base64url_decode(header_segment)
|
||||
except (TypeError, binascii.Error):
|
||||
raise DecodeError('Invalid header padding')
|
||||
|
||||
try:
|
||||
header = json.loads(header_data.decode('utf-8'))
|
||||
except ValueError as e:
|
||||
raise DecodeError('Invalid header string: %s' % e)
|
||||
|
||||
if not isinstance(header, Mapping):
|
||||
raise DecodeError('Invalid header string: must be a json object')
|
||||
|
||||
try:
|
||||
payload = base64url_decode(payload_segment)
|
||||
except (TypeError, binascii.Error):
|
||||
raise DecodeError('Invalid payload padding')
|
||||
|
||||
try:
|
||||
signature = base64url_decode(crypto_segment)
|
||||
except (TypeError, binascii.Error):
|
||||
raise DecodeError('Invalid crypto padding')
|
||||
|
||||
return (payload, signing_input, header, signature)
|
||||
|
||||
def _verify_signature(self, payload, signing_input, header, signature,
|
||||
key='', algorithms=None):
|
||||
|
||||
alg = header.get('alg')
|
||||
|
||||
if algorithms is not None and alg not in algorithms:
|
||||
raise InvalidAlgorithmError('The specified alg value is not allowed')
|
||||
|
||||
try:
|
||||
alg_obj = self._algorithms[alg]
|
||||
key = alg_obj.prepare_key(key)
|
||||
|
||||
if not alg_obj.verify(signing_input, key, signature):
|
||||
raise DecodeError('Signature verification failed')
|
||||
|
||||
except KeyError:
|
||||
raise InvalidAlgorithmError('Algorithm not supported')
|
||||
|
||||
def _validate_headers(self, headers):
|
||||
if 'kid' in headers:
|
||||
self._validate_kid(headers['kid'])
|
||||
|
||||
def _validate_kid(self, kid):
|
||||
if not isinstance(kid, string_types):
|
||||
raise InvalidTokenError('Key ID header parameter must be a string')
|
||||
|
||||
|
||||
_jws_global_obj = PyJWS()
|
||||
encode = _jws_global_obj.encode
|
||||
decode = _jws_global_obj.decode
|
||||
register_algorithm = _jws_global_obj.register_algorithm
|
||||
unregister_algorithm = _jws_global_obj.unregister_algorithm
|
||||
get_unverified_header = _jws_global_obj.get_unverified_header
|
||||
@@ -0,0 +1,199 @@
|
||||
import json
|
||||
import warnings
|
||||
|
||||
from calendar import timegm
|
||||
from collections import Mapping
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from .api_jws import PyJWS
|
||||
from .algorithms import Algorithm, get_default_algorithms # NOQA
|
||||
from .compat import string_types, timedelta_total_seconds
|
||||
from .exceptions import (
|
||||
DecodeError, ExpiredSignatureError, ImmatureSignatureError,
|
||||
InvalidAudienceError, InvalidIssuedAtError,
|
||||
InvalidIssuerError, MissingRequiredClaimError
|
||||
)
|
||||
from .utils import merge_dict
|
||||
|
||||
|
||||
class PyJWT(PyJWS):
|
||||
header_type = 'JWT'
|
||||
|
||||
@staticmethod
|
||||
def _get_default_options():
|
||||
return {
|
||||
'verify_signature': True,
|
||||
'verify_exp': True,
|
||||
'verify_nbf': True,
|
||||
'verify_iat': True,
|
||||
'verify_aud': True,
|
||||
'verify_iss': True,
|
||||
'require_exp': False,
|
||||
'require_iat': False,
|
||||
'require_nbf': False
|
||||
}
|
||||
|
||||
def encode(self, payload, key, algorithm='HS256', headers=None,
|
||||
json_encoder=None):
|
||||
# Check that we get a mapping
|
||||
if not isinstance(payload, Mapping):
|
||||
raise TypeError('Expecting a mapping object, as JWT only supports '
|
||||
'JSON objects as payloads.')
|
||||
|
||||
# Payload
|
||||
for time_claim in ['exp', 'iat', 'nbf']:
|
||||
# Convert datetime to a intDate value in known time-format claims
|
||||
if isinstance(payload.get(time_claim), datetime):
|
||||
payload[time_claim] = timegm(payload[time_claim].utctimetuple())
|
||||
|
||||
json_payload = json.dumps(
|
||||
payload,
|
||||
separators=(',', ':'),
|
||||
cls=json_encoder
|
||||
).encode('utf-8')
|
||||
|
||||
return super(PyJWT, self).encode(
|
||||
json_payload, key, algorithm, headers, json_encoder
|
||||
)
|
||||
|
||||
def decode(self, jwt, key='', verify=True, algorithms=None, options=None,
|
||||
**kwargs):
|
||||
|
||||
if verify and not algorithms:
|
||||
warnings.warn(
|
||||
'It is strongly recommended that you pass in a ' +
|
||||
'value for the "algorithms" argument when calling decode(). ' +
|
||||
'This argument will be mandatory in a future version.',
|
||||
DeprecationWarning
|
||||
)
|
||||
|
||||
payload, signing_input, header, signature = self._load(jwt)
|
||||
|
||||
if options is None:
|
||||
options = {'verify_signature': verify}
|
||||
else:
|
||||
options.setdefault('verify_signature', verify)
|
||||
|
||||
decoded = super(PyJWT, self).decode(
|
||||
jwt, key=key, algorithms=algorithms, options=options, **kwargs
|
||||
)
|
||||
|
||||
try:
|
||||
payload = json.loads(decoded.decode('utf-8'))
|
||||
except ValueError as e:
|
||||
raise DecodeError('Invalid payload string: %s' % e)
|
||||
if not isinstance(payload, Mapping):
|
||||
raise DecodeError('Invalid payload string: must be a json object')
|
||||
|
||||
if verify:
|
||||
merged_options = merge_dict(self.options, options)
|
||||
self._validate_claims(payload, merged_options, **kwargs)
|
||||
|
||||
return payload
|
||||
|
||||
def _validate_claims(self, payload, options, audience=None, issuer=None,
|
||||
leeway=0, **kwargs):
|
||||
|
||||
if 'verify_expiration' in kwargs:
|
||||
options['verify_exp'] = kwargs.get('verify_expiration', True)
|
||||
warnings.warn('The verify_expiration parameter is deprecated. '
|
||||
'Please use verify_exp in options instead.',
|
||||
DeprecationWarning)
|
||||
|
||||
if isinstance(leeway, timedelta):
|
||||
leeway = timedelta_total_seconds(leeway)
|
||||
|
||||
if not isinstance(audience, (string_types, type(None))):
|
||||
raise TypeError('audience must be a string or None')
|
||||
|
||||
self._validate_required_claims(payload, options)
|
||||
|
||||
now = timegm(datetime.utcnow().utctimetuple())
|
||||
|
||||
if 'iat' in payload and options.get('verify_iat'):
|
||||
self._validate_iat(payload, now, leeway)
|
||||
|
||||
if 'nbf' in payload and options.get('verify_nbf'):
|
||||
self._validate_nbf(payload, now, leeway)
|
||||
|
||||
if 'exp' in payload and options.get('verify_exp'):
|
||||
self._validate_exp(payload, now, leeway)
|
||||
|
||||
if options.get('verify_iss'):
|
||||
self._validate_iss(payload, issuer)
|
||||
|
||||
if options.get('verify_aud'):
|
||||
self._validate_aud(payload, audience)
|
||||
|
||||
def _validate_required_claims(self, payload, options):
|
||||
if options.get('require_exp') and payload.get('exp') is None:
|
||||
raise MissingRequiredClaimError('exp')
|
||||
|
||||
if options.get('require_iat') and payload.get('iat') is None:
|
||||
raise MissingRequiredClaimError('iat')
|
||||
|
||||
if options.get('require_nbf') and payload.get('nbf') is None:
|
||||
raise MissingRequiredClaimError('nbf')
|
||||
|
||||
def _validate_iat(self, payload, now, leeway):
|
||||
try:
|
||||
int(payload['iat'])
|
||||
except ValueError:
|
||||
raise InvalidIssuedAtError('Issued At claim (iat) must be an integer.')
|
||||
|
||||
def _validate_nbf(self, payload, now, leeway):
|
||||
try:
|
||||
nbf = int(payload['nbf'])
|
||||
except ValueError:
|
||||
raise DecodeError('Not Before claim (nbf) must be an integer.')
|
||||
|
||||
if nbf > (now + leeway):
|
||||
raise ImmatureSignatureError('The token is not yet valid (nbf)')
|
||||
|
||||
def _validate_exp(self, payload, now, leeway):
|
||||
try:
|
||||
exp = int(payload['exp'])
|
||||
except ValueError:
|
||||
raise DecodeError('Expiration Time claim (exp) must be an'
|
||||
' integer.')
|
||||
|
||||
if exp < (now - leeway):
|
||||
raise ExpiredSignatureError('Signature has expired')
|
||||
|
||||
def _validate_aud(self, payload, audience):
|
||||
if audience is None and 'aud' not in payload:
|
||||
return
|
||||
|
||||
if audience is not None and 'aud' not in payload:
|
||||
# Application specified an audience, but it could not be
|
||||
# verified since the token does not contain a claim.
|
||||
raise MissingRequiredClaimError('aud')
|
||||
|
||||
audience_claims = payload['aud']
|
||||
|
||||
if isinstance(audience_claims, string_types):
|
||||
audience_claims = [audience_claims]
|
||||
if not isinstance(audience_claims, list):
|
||||
raise InvalidAudienceError('Invalid claim format in token')
|
||||
if any(not isinstance(c, string_types) for c in audience_claims):
|
||||
raise InvalidAudienceError('Invalid claim format in token')
|
||||
if audience not in audience_claims:
|
||||
raise InvalidAudienceError('Invalid audience')
|
||||
|
||||
def _validate_iss(self, payload, issuer):
|
||||
if issuer is None:
|
||||
return
|
||||
|
||||
if 'iss' not in payload:
|
||||
raise MissingRequiredClaimError('iss')
|
||||
|
||||
if payload['iss'] != issuer:
|
||||
raise InvalidIssuerError('Invalid issuer')
|
||||
|
||||
|
||||
_jwt_global_obj = PyJWT()
|
||||
encode = _jwt_global_obj.encode
|
||||
decode = _jwt_global_obj.decode
|
||||
register_algorithm = _jwt_global_obj.register_algorithm
|
||||
unregister_algorithm = _jwt_global_obj.unregister_algorithm
|
||||
get_unverified_header = _jwt_global_obj.get_unverified_header
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
The `compat` module provides support for backwards compatibility with older
|
||||
versions of python, and compatibility wrappers around optional packages.
|
||||
"""
|
||||
# flake8: noqa
|
||||
import hmac
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
|
||||
if PY3:
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
else:
|
||||
text_type = unicode
|
||||
binary_type = str
|
||||
|
||||
string_types = (text_type, binary_type)
|
||||
|
||||
|
||||
def timedelta_total_seconds(delta):
|
||||
try:
|
||||
delta.total_seconds
|
||||
except AttributeError:
|
||||
# On Python 2.6, timedelta instances do not have
|
||||
# a .total_seconds() method.
|
||||
total_seconds = delta.days * 24 * 60 * 60 + delta.seconds
|
||||
else:
|
||||
total_seconds = delta.total_seconds()
|
||||
|
||||
return total_seconds
|
||||
|
||||
|
||||
try:
|
||||
constant_time_compare = hmac.compare_digest
|
||||
except AttributeError:
|
||||
# Fallback for Python < 2.7
|
||||
def constant_time_compare(val1, val2):
|
||||
"""
|
||||
Returns True if the two strings are equal, False otherwise.
|
||||
|
||||
The time taken is independent of the number of characters that match.
|
||||
"""
|
||||
if len(val1) != len(val2):
|
||||
return False
|
||||
|
||||
result = 0
|
||||
|
||||
for x, y in zip(val1, val2):
|
||||
result |= ord(x) ^ ord(y)
|
||||
|
||||
return result == 0
|
||||
|
||||
# Use int.to_bytes if it exists (Python 3)
|
||||
if getattr(int, 'to_bytes', None):
|
||||
def bytes_from_int(val):
|
||||
remaining = val
|
||||
byte_length = 0
|
||||
|
||||
while remaining != 0:
|
||||
remaining = remaining >> 8
|
||||
byte_length += 1
|
||||
|
||||
return val.to_bytes(byte_length, 'big', signed=False)
|
||||
else:
|
||||
def bytes_from_int(val):
|
||||
buf = []
|
||||
while val:
|
||||
val, remainder = divmod(val, 256)
|
||||
buf.append(remainder)
|
||||
|
||||
buf.reverse()
|
||||
return struct.pack('%sB' % len(buf), *buf)
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,60 @@
|
||||
# Note: This file is named py_ecdsa.py because import behavior in Python 2
|
||||
# would cause ecdsa.py to squash the ecdsa library that it depends upon.
|
||||
|
||||
import hashlib
|
||||
|
||||
import ecdsa
|
||||
|
||||
from jwt.algorithms import Algorithm
|
||||
from jwt.compat import string_types, text_type
|
||||
|
||||
|
||||
class ECAlgorithm(Algorithm):
|
||||
"""
|
||||
Performs signing and verification operations using
|
||||
ECDSA and the specified hash function
|
||||
|
||||
This class requires the ecdsa package to be installed.
|
||||
|
||||
This is based off of the implementation in PyJWT 0.3.2
|
||||
"""
|
||||
SHA256 = hashlib.sha256
|
||||
SHA384 = hashlib.sha384
|
||||
SHA512 = hashlib.sha512
|
||||
|
||||
def __init__(self, hash_alg):
|
||||
self.hash_alg = hash_alg
|
||||
|
||||
def prepare_key(self, key):
|
||||
|
||||
if isinstance(key, ecdsa.SigningKey) or \
|
||||
isinstance(key, ecdsa.VerifyingKey):
|
||||
return key
|
||||
|
||||
if isinstance(key, string_types):
|
||||
if isinstance(key, text_type):
|
||||
key = key.encode('utf-8')
|
||||
|
||||
# Attempt to load key. We don't know if it's
|
||||
# a Signing Key or a Verifying Key, so we try
|
||||
# the Verifying Key first.
|
||||
try:
|
||||
key = ecdsa.VerifyingKey.from_pem(key)
|
||||
except ecdsa.der.UnexpectedDER:
|
||||
key = ecdsa.SigningKey.from_pem(key)
|
||||
|
||||
else:
|
||||
raise TypeError('Expecting a PEM-formatted key.')
|
||||
|
||||
return key
|
||||
|
||||
def sign(self, msg, key):
|
||||
return key.sign(msg, hashfunc=self.hash_alg,
|
||||
sigencode=ecdsa.util.sigencode_string)
|
||||
|
||||
def verify(self, msg, key, sig):
|
||||
try:
|
||||
return key.verify(sig, msg, hashfunc=self.hash_alg,
|
||||
sigdecode=ecdsa.util.sigdecode_string)
|
||||
except AssertionError:
|
||||
return False
|
||||
@@ -0,0 +1,47 @@
|
||||
import Crypto.Hash.SHA256
|
||||
import Crypto.Hash.SHA384
|
||||
import Crypto.Hash.SHA512
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
|
||||
from jwt.algorithms import Algorithm
|
||||
from jwt.compat import string_types, text_type
|
||||
|
||||
|
||||
class RSAAlgorithm(Algorithm):
|
||||
"""
|
||||
Performs signing and verification operations using
|
||||
RSASSA-PKCS-v1_5 and the specified hash function.
|
||||
|
||||
This class requires PyCrypto package to be installed.
|
||||
|
||||
This is based off of the implementation in PyJWT 0.3.2
|
||||
"""
|
||||
SHA256 = Crypto.Hash.SHA256
|
||||
SHA384 = Crypto.Hash.SHA384
|
||||
SHA512 = Crypto.Hash.SHA512
|
||||
|
||||
def __init__(self, hash_alg):
|
||||
self.hash_alg = hash_alg
|
||||
|
||||
def prepare_key(self, key):
|
||||
|
||||
if isinstance(key, RSA._RSAobj):
|
||||
return key
|
||||
|
||||
if isinstance(key, string_types):
|
||||
if isinstance(key, text_type):
|
||||
key = key.encode('utf-8')
|
||||
|
||||
key = RSA.importKey(key)
|
||||
else:
|
||||
raise TypeError('Expecting a PEM- or RSA-formatted key.')
|
||||
|
||||
return key
|
||||
|
||||
def sign(self, msg, key):
|
||||
return PKCS1_v1_5.new(key).sign(self.hash_alg.new(msg))
|
||||
|
||||
def verify(self, msg, key, sig):
|
||||
return PKCS1_v1_5.new(key).verify(self.hash_alg.new(msg), sig)
|
||||
@@ -0,0 +1,48 @@
|
||||
class InvalidTokenError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DecodeError(InvalidTokenError):
|
||||
pass
|
||||
|
||||
|
||||
class ExpiredSignatureError(InvalidTokenError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAudienceError(InvalidTokenError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidIssuerError(InvalidTokenError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidIssuedAtError(InvalidTokenError):
|
||||
pass
|
||||
|
||||
|
||||
class ImmatureSignatureError(InvalidTokenError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidKeyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAlgorithmError(InvalidTokenError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingRequiredClaimError(InvalidTokenError):
|
||||
def __init__(self, claim):
|
||||
self.claim = claim
|
||||
|
||||
def __str__(self):
|
||||
return 'Token is missing the "%s" claim' % self.claim
|
||||
|
||||
|
||||
# Compatibility aliases (deprecated)
|
||||
ExpiredSignature = ExpiredSignatureError
|
||||
InvalidAudience = InvalidAudienceError
|
||||
InvalidIssuer = InvalidIssuerError
|
||||
@@ -0,0 +1,113 @@
|
||||
import base64
|
||||
import binascii
|
||||
import struct
|
||||
|
||||
from .compat import binary_type, bytes_from_int, text_type
|
||||
|
||||
try:
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import (
|
||||
decode_dss_signature, encode_dss_signature
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def force_unicode(value):
|
||||
if isinstance(value, binary_type):
|
||||
return value.decode('utf-8')
|
||||
elif isinstance(value, text_type):
|
||||
return value
|
||||
else:
|
||||
raise TypeError('Expected a string value')
|
||||
|
||||
|
||||
def force_bytes(value):
|
||||
if isinstance(value, text_type):
|
||||
return value.encode('utf-8')
|
||||
elif isinstance(value, binary_type):
|
||||
return value
|
||||
else:
|
||||
raise TypeError('Expected a string value')
|
||||
|
||||
|
||||
def base64url_decode(input):
|
||||
if isinstance(input, text_type):
|
||||
input = input.encode('ascii')
|
||||
|
||||
rem = len(input) % 4
|
||||
|
||||
if rem > 0:
|
||||
input += b'=' * (4 - rem)
|
||||
|
||||
return base64.urlsafe_b64decode(input)
|
||||
|
||||
|
||||
def base64url_encode(input):
|
||||
return base64.urlsafe_b64encode(input).replace(b'=', b'')
|
||||
|
||||
|
||||
def to_base64url_uint(val):
|
||||
if val < 0:
|
||||
raise ValueError('Must be a positive integer')
|
||||
|
||||
int_bytes = bytes_from_int(val)
|
||||
|
||||
if len(int_bytes) == 0:
|
||||
int_bytes = b'\x00'
|
||||
|
||||
return base64url_encode(int_bytes)
|
||||
|
||||
|
||||
def from_base64url_uint(val):
|
||||
if isinstance(val, text_type):
|
||||
val = val.encode('ascii')
|
||||
|
||||
data = base64url_decode(val)
|
||||
|
||||
buf = struct.unpack('%sB' % len(data), data)
|
||||
return int(''.join(["%02x" % byte for byte in buf]), 16)
|
||||
|
||||
|
||||
def merge_dict(original, updates):
|
||||
if not updates:
|
||||
return original
|
||||
|
||||
try:
|
||||
merged_options = original.copy()
|
||||
merged_options.update(updates)
|
||||
except (AttributeError, ValueError) as e:
|
||||
raise TypeError('original and updates must be a dictionary: %s' % e)
|
||||
|
||||
return merged_options
|
||||
|
||||
|
||||
def number_to_bytes(num, num_bytes):
|
||||
padded_hex = '%0*x' % (2 * num_bytes, num)
|
||||
big_endian = binascii.a2b_hex(padded_hex.encode('ascii'))
|
||||
return big_endian
|
||||
|
||||
|
||||
def bytes_to_number(string):
|
||||
return int(binascii.b2a_hex(string), 16)
|
||||
|
||||
|
||||
def der_to_raw_signature(der_sig, curve):
|
||||
num_bits = curve.key_size
|
||||
num_bytes = (num_bits + 7) // 8
|
||||
|
||||
r, s = decode_dss_signature(der_sig)
|
||||
|
||||
return number_to_bytes(r, num_bytes) + number_to_bytes(s, num_bytes)
|
||||
|
||||
|
||||
def raw_to_der_signature(raw_sig, curve):
|
||||
num_bits = curve.key_size
|
||||
num_bytes = (num_bits + 7) // 8
|
||||
|
||||
if len(raw_sig) != 2 * num_bytes:
|
||||
raise ValueError('Invalid signature')
|
||||
|
||||
r = bytes_to_number(raw_sig[:num_bytes])
|
||||
s = bytes_to_number(raw_sig[num_bytes:])
|
||||
|
||||
return encode_dss_signature(r, s)
|
||||
@@ -0,0 +1,103 @@
|
||||
OAuthLib
|
||||
========
|
||||
|
||||
*A generic, spec-compliant, thorough implementation of the OAuth request-signing
|
||||
logic for python*
|
||||
|
||||
.. image:: https://travis-ci.org/idan/oauthlib.svg?branch=master
|
||||
:target: https://travis-ci.org/idan/oauthlib
|
||||
.. image:: https://coveralls.io/repos/idan/oauthlib/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/idan/oauthlib
|
||||
|
||||
|
||||
OAuth often seems complicated and difficult-to-implement. There are several
|
||||
prominent libraries for handling OAuth requests, but they all suffer from one or
|
||||
both of the following:
|
||||
|
||||
1. They predate the `OAuth 1.0 spec`_, AKA RFC 5849.
|
||||
2. They predate the `OAuth 2.0 spec`_, AKA RFC 6749.
|
||||
3. They assume the usage of a specific HTTP request library.
|
||||
|
||||
.. _`OAuth 1.0 spec`: http://tools.ietf.org/html/rfc5849
|
||||
.. _`OAuth 2.0 spec`: http://tools.ietf.org/html/rfc6749
|
||||
|
||||
OAuthLib is a generic utility which implements the logic of OAuth without
|
||||
assuming a specific HTTP request object or web framework. Use it to graft OAuth
|
||||
client support onto your favorite HTTP library, or provide support onto your
|
||||
favourite web framework. If you're a maintainer of such a library, write a thin
|
||||
veneer on top of OAuthLib and get OAuth support for very little effort.
|
||||
|
||||
|
||||
Documentation
|
||||
--------------
|
||||
|
||||
Full documentation is available on `Read the Docs`_. All contributions are very
|
||||
welcome! The documentation is still quite sparse, please open an issue for what
|
||||
you'd like to know, or discuss it in our `G+ community`_, or even better, send a
|
||||
pull request!
|
||||
|
||||
.. _`G+ community`: https://plus.google.com/communities/101889017375384052571
|
||||
.. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html
|
||||
|
||||
Interested in making OAuth requests?
|
||||
------------------------------------
|
||||
|
||||
Then you might be more interested in using `requests`_ which has OAuthLib
|
||||
powered OAuth support provided by the `requests-oauthlib`_ library.
|
||||
|
||||
.. _`requests`: https://github.com/kennethreitz/requests
|
||||
.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib
|
||||
|
||||
Which web frameworks are supported?
|
||||
-----------------------------------
|
||||
|
||||
The following packages provide OAuth support using OAuthLib.
|
||||
|
||||
- For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support.
|
||||
- For Flask there is `flask-oauthlib`_ and `Flask-Dance`_.
|
||||
- For Pyramid there is `pyramid-oauthlib`_.
|
||||
|
||||
If you have written an OAuthLib package that supports your favorite framework,
|
||||
please open a Pull Request, updating the documentation.
|
||||
|
||||
.. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit
|
||||
.. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib
|
||||
.. _`Django REST framework`: http://django-rest-framework.org
|
||||
.. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance
|
||||
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
|
||||
|
||||
Using OAuthLib? Please get in touch!
|
||||
------------------------------------
|
||||
Patching OAuth support onto an http request framework? Creating an OAuth
|
||||
provider extension for a web framework? Simply using OAuthLib to Get Things Done
|
||||
or to learn?
|
||||
|
||||
No matter which we'd love to hear from you in our `G+ community`_ or if you have
|
||||
anything in particular you would like to have, change or comment on don't
|
||||
hesitate for a second to send a pull request or open an issue. We might be quite
|
||||
busy and therefore slow to reply but we love feedback!
|
||||
|
||||
Chances are you have run into something annoying that you wish there was
|
||||
documentation for, if you wish to gain eternal fame and glory, and a drink if we
|
||||
have the pleasure to run into eachother, please send a docs pull request =)
|
||||
|
||||
.. _`G+ community`: https://plus.google.com/communities/101889017375384052571
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
OAuthLib is yours to use and abuse according to the terms of the BSD license.
|
||||
Check the LICENSE file for full details.
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
*OAuthLib is in active development, with the core of both OAuth 1 and 2
|
||||
completed, for providers as well as clients.* See `supported features`_ for
|
||||
details.
|
||||
|
||||
.. _`supported features`: https://oauthlib.readthedocs.io/en/latest/feature_matrix.html
|
||||
|
||||
For a full changelog see ``CHANGELOG.rst``.
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
@@ -0,0 +1,144 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: oauthlib
|
||||
Version: 2.0.6
|
||||
Summary: A generic, spec-compliant, thorough implementation of the OAuth request-signing logic
|
||||
Home-page: https://github.com/idan/oauthlib
|
||||
Author: Ib Lundgren
|
||||
Author-email: ib.lundgren@gmail.com
|
||||
License: BSD
|
||||
Platform: any
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Environment :: Web Environment
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved
|
||||
Classifier: License :: OSI Approved :: BSD License
|
||||
Classifier: Operating System :: MacOS
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
Classifier: Programming Language :: Python :: 3.6
|
||||
Classifier: Programming Language :: Python :: Implementation
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Provides-Extra: rsa
|
||||
Requires-Dist: cryptography; extra == 'rsa'
|
||||
Provides-Extra: signals
|
||||
Requires-Dist: blinker; extra == 'signals'
|
||||
Provides-Extra: signedtoken
|
||||
Requires-Dist: cryptography; extra == 'signedtoken'
|
||||
Requires-Dist: pyjwt (>=1.0.0); extra == 'signedtoken'
|
||||
Provides-Extra: test
|
||||
Requires-Dist: nose; extra == 'test'
|
||||
Requires-Dist: cryptography; extra == 'test'
|
||||
Requires-Dist: pyjwt (>=1.0.0); extra == 'test'
|
||||
Requires-Dist: blinker; extra == 'test'
|
||||
|
||||
OAuthLib
|
||||
========
|
||||
|
||||
*A generic, spec-compliant, thorough implementation of the OAuth request-signing
|
||||
logic for python*
|
||||
|
||||
.. image:: https://travis-ci.org/idan/oauthlib.svg?branch=master
|
||||
:target: https://travis-ci.org/idan/oauthlib
|
||||
.. image:: https://coveralls.io/repos/idan/oauthlib/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/idan/oauthlib
|
||||
|
||||
|
||||
OAuth often seems complicated and difficult-to-implement. There are several
|
||||
prominent libraries for handling OAuth requests, but they all suffer from one or
|
||||
both of the following:
|
||||
|
||||
1. They predate the `OAuth 1.0 spec`_, AKA RFC 5849.
|
||||
2. They predate the `OAuth 2.0 spec`_, AKA RFC 6749.
|
||||
3. They assume the usage of a specific HTTP request library.
|
||||
|
||||
.. _`OAuth 1.0 spec`: http://tools.ietf.org/html/rfc5849
|
||||
.. _`OAuth 2.0 spec`: http://tools.ietf.org/html/rfc6749
|
||||
|
||||
OAuthLib is a generic utility which implements the logic of OAuth without
|
||||
assuming a specific HTTP request object or web framework. Use it to graft OAuth
|
||||
client support onto your favorite HTTP library, or provide support onto your
|
||||
favourite web framework. If you're a maintainer of such a library, write a thin
|
||||
veneer on top of OAuthLib and get OAuth support for very little effort.
|
||||
|
||||
|
||||
Documentation
|
||||
--------------
|
||||
|
||||
Full documentation is available on `Read the Docs`_. All contributions are very
|
||||
welcome! The documentation is still quite sparse, please open an issue for what
|
||||
you'd like to know, or discuss it in our `G+ community`_, or even better, send a
|
||||
pull request!
|
||||
|
||||
.. _`G+ community`: https://plus.google.com/communities/101889017375384052571
|
||||
.. _`Read the Docs`: https://oauthlib.readthedocs.io/en/latest/index.html
|
||||
|
||||
Interested in making OAuth requests?
|
||||
------------------------------------
|
||||
|
||||
Then you might be more interested in using `requests`_ which has OAuthLib
|
||||
powered OAuth support provided by the `requests-oauthlib`_ library.
|
||||
|
||||
.. _`requests`: https://github.com/kennethreitz/requests
|
||||
.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib
|
||||
|
||||
Which web frameworks are supported?
|
||||
-----------------------------------
|
||||
|
||||
The following packages provide OAuth support using OAuthLib.
|
||||
|
||||
- For Django there is `django-oauth-toolkit`_, which includes `Django REST framework`_ support.
|
||||
- For Flask there is `flask-oauthlib`_ and `Flask-Dance`_.
|
||||
- For Pyramid there is `pyramid-oauthlib`_.
|
||||
|
||||
If you have written an OAuthLib package that supports your favorite framework,
|
||||
please open a Pull Request, updating the documentation.
|
||||
|
||||
.. _`django-oauth-toolkit`: https://github.com/evonove/django-oauth-toolkit
|
||||
.. _`flask-oauthlib`: https://github.com/lepture/flask-oauthlib
|
||||
.. _`Django REST framework`: http://django-rest-framework.org
|
||||
.. _`Flask-Dance`: https://github.com/singingwolfboy/flask-dance
|
||||
.. _`pyramid-oauthlib`: https://github.com/tilgovi/pyramid-oauthlib
|
||||
|
||||
Using OAuthLib? Please get in touch!
|
||||
------------------------------------
|
||||
Patching OAuth support onto an http request framework? Creating an OAuth
|
||||
provider extension for a web framework? Simply using OAuthLib to Get Things Done
|
||||
or to learn?
|
||||
|
||||
No matter which we'd love to hear from you in our `G+ community`_ or if you have
|
||||
anything in particular you would like to have, change or comment on don't
|
||||
hesitate for a second to send a pull request or open an issue. We might be quite
|
||||
busy and therefore slow to reply but we love feedback!
|
||||
|
||||
Chances are you have run into something annoying that you wish there was
|
||||
documentation for, if you wish to gain eternal fame and glory, and a drink if we
|
||||
have the pleasure to run into eachother, please send a docs pull request =)
|
||||
|
||||
.. _`G+ community`: https://plus.google.com/communities/101889017375384052571
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
OAuthLib is yours to use and abuse according to the terms of the BSD license.
|
||||
Check the LICENSE file for full details.
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
*OAuthLib is in active development, with the core of both OAuth 1 and 2
|
||||
completed, for providers as well as clients.* See `supported features`_ for
|
||||
details.
|
||||
|
||||
.. _`supported features`: https://oauthlib.readthedocs.io/en/latest/feature_matrix.html
|
||||
|
||||
For a full changelog see ``CHANGELOG.rst``.
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
oauthlib/__init__.py,sha256=-9EAes_pwDq1unCVb7ZMdmT68N45zF_GcKpzsod7qUo,520
|
||||
oauthlib/common.py,sha256=60Q-e05N0AN8w9H5cyhKZRTzlm5ep1YzTHlstFGdbuQ,14553
|
||||
oauthlib/signals.py,sha256=WdwbOYf_bsZAIWAfxxEiKixqm9a56rCkjAmgyw1JQiE,1521
|
||||
oauthlib/uri_validate.py,sha256=PpEpmrRxA1w-H3oicm0vqsg9dM9hjGRx1wxh96Xnl7k,7611
|
||||
oauthlib/oauth1/__init__.py,sha256=DHwYb0S5eNIqm6VXql7SaoFywtW6d9MgtGZAzZyOTk4,842
|
||||
oauthlib/oauth1/rfc5849/__init__.py,sha256=pwBGWl9FV0OzcxRjS3I-RVUXRipL54k9y532Ut2Oi2U,15245
|
||||
oauthlib/oauth1/rfc5849/errors.py,sha256=9BqHAtzoVUQ7v0SBkT3COFt2JuwMZlOOEXtL0pv7d4I,2543
|
||||
oauthlib/oauth1/rfc5849/parameters.py,sha256=Et08qMVoHuLSOHTNwl32ITn24xdN-9cioyces2jnJO8,4951
|
||||
oauthlib/oauth1/rfc5849/request_validator.py,sha256=F0xmtIhQpRTbF3HCGbmmIgpw7FVT4brWGiL0_wukk6s,30459
|
||||
oauthlib/oauth1/rfc5849/signature.py,sha256=2xaP5rW64x-JMXklCJNzBNa7_B2UkbOiEmyMqe7Brn4,23580
|
||||
oauthlib/oauth1/rfc5849/utils.py,sha256=AAaon0zxFLsazJrX5767Y5JTo2fu-_aSMbxHLskRAm8,2781
|
||||
oauthlib/oauth1/rfc5849/endpoints/__init__.py,sha256=d1ZrqNd-gGcMFKILLIE3yVoQVR-bx9AQaj0OqPrX-qc,352
|
||||
oauthlib/oauth1/rfc5849/endpoints/access_token.py,sha256=Z71p0t614L8p28YnL7HcRuM1e2C0rOCXj8La4qiO6Go,9344
|
||||
oauthlib/oauth1/rfc5849/endpoints/authorization.py,sha256=MaSkwWYuClN3JH-7DNsKJFwDUT3HbJhPvKynkNQ6J2o,6823
|
||||
oauthlib/oauth1/rfc5849/endpoints/base.py,sha256=1wy8_egRexQoEbrDNEGjvO5mpbu4fUqVMmxFblfxuGE,10352
|
||||
oauthlib/oauth1/rfc5849/endpoints/pre_configured.py,sha256=ttawzUkqmjvu1OlCw-fMDEFJNJYXk0OAtZYPBQDZc4E,605
|
||||
oauthlib/oauth1/rfc5849/endpoints/request_token.py,sha256=XgU8-UNeINr4nFLKt9mgl4jz5nySkLzMnLcuVTAe6BA,9283
|
||||
oauthlib/oauth1/rfc5849/endpoints/resource.py,sha256=Xf7peXSpb82JRxp6J80Gm2r2OY_s6uewPYI6o70Kqfs,7433
|
||||
oauthlib/oauth1/rfc5849/endpoints/signature_only.py,sha256=1gVLRB6MFpa-hTlzaR0wfAKSpD53t-7sGww_xwTpV4o,3385
|
||||
oauthlib/oauth2/__init__.py,sha256=h-aY5iK2_DiM4M0yOTyCfCqQKxrK4WASGA19b0B6w6k,2141
|
||||
oauthlib/oauth2/rfc6749/__init__.py,sha256=ByRccoeZxOuCI5LVDLkzJKzyrzSYyqX6yNbAJrRWSnQ,1728
|
||||
oauthlib/oauth2/rfc6749/errors.py,sha256=ps9pEcHNT0vkquAGyJpUOesYyixLIH6EMvnWSCH6pCc,12866
|
||||
oauthlib/oauth2/rfc6749/parameters.py,sha256=hyhqQW2Yf_elrFht1Zqm3W40str6-GcecqLCuilF-D8,15910
|
||||
oauthlib/oauth2/rfc6749/request_validator.py,sha256=ljJLEfkGFq7Llups1fj9zEUCRvC6cU4Ifrwk2VIflZc,24574
|
||||
oauthlib/oauth2/rfc6749/tokens.py,sha256=biyELAT9L-hxVIGd51QDE8VdpQqNKURmrBHa1ypTiWk,9798
|
||||
oauthlib/oauth2/rfc6749/utils.py,sha256=TbyPVOw_hfBrf3A82HI94bEpHb0HA5At_uMg4neskEo,2494
|
||||
oauthlib/oauth2/rfc6749/clients/__init__.py,sha256=MBwNpK8to49QlYYMxhHooU4UnPG6hsKpAVwoCm-ufE4,562
|
||||
oauthlib/oauth2/rfc6749/clients/backend_application.py,sha256=kDE80WNI0Lkhmkzz2rz2GeYmWwb6Mze9zrgFGyyQO2g,2474
|
||||
oauthlib/oauth2/rfc6749/clients/base.py,sha256=OsMMi8uIEFAIrLvwZyzfAGjrlaQqE671DvDACevtgp8,20354
|
||||
oauthlib/oauth2/rfc6749/clients/legacy_application.py,sha256=rlG_a9z1mE-mZfT8IUm2tJ0wcJp5tQTf_9w-cfikJ1E,3313
|
||||
oauthlib/oauth2/rfc6749/clients/mobile_application.py,sha256=cWosPAJZ7w7SxGaCPuDUArbzUCAoob28e1o0Mvxy7Hg,8769
|
||||
oauthlib/oauth2/rfc6749/clients/service_application.py,sha256=KtB3m70qP8lDQ9Jf-kaEsuFOFWKNdDaMkoVm4MZMF9Q,6980
|
||||
oauthlib/oauth2/rfc6749/clients/web_application.py,sha256=4eC57KbS0Jquf8dzxJpFlg5XNC1Mn1HJX5zqGE2JCls,9201
|
||||
oauthlib/oauth2/rfc6749/endpoints/__init__.py,sha256=2cFcinSijL_H9ToMCwc8aYTYQhpMnNvUtXY8LNGMYZ0,648
|
||||
oauthlib/oauth2/rfc6749/endpoints/authorization.py,sha256=zlM8P1TaH-LqMOj-gIhtATMsHF8BqoHw8ws1P7eEs4w,4665
|
||||
oauthlib/oauth2/rfc6749/endpoints/base.py,sha256=L2QooEesOsDIlMvR2jPStG31p1_bu9qX0Ab3VCQw_So,1733
|
||||
oauthlib/oauth2/rfc6749/endpoints/pre_configured.py,sha256=OccCP8eMhrT6ReyKQs8PHxH1bDjirfyj8wWKM-tn7MA,12327
|
||||
oauthlib/oauth2/rfc6749/endpoints/resource.py,sha256=4Br4ZvzTc4mhVW9Bsw_gHQNA-BwAyr0_-r6GotaCifA,3316
|
||||
oauthlib/oauth2/rfc6749/endpoints/revocation.py,sha256=F38BPAQ_HUyavfUvwEU65TUwdJ7Msj-ILsFqnvfdftI,5796
|
||||
oauthlib/oauth2/rfc6749/endpoints/token.py,sha256=2uZlbQ6BN88UXhSS4iceJJM7Lh0kl9_NijeEPyUdN3k,4516
|
||||
oauthlib/oauth2/rfc6749/grant_types/__init__.py,sha256=Y4ejhY509WtGNwi6S2_cCiY_xdEIAxnvsQGDMdT1XCc,728
|
||||
oauthlib/oauth2/rfc6749/grant_types/authorization_code.py,sha256=0Wva1RB4lWAjUXb-YZMPE5PZvr90jeiFxwSOwwaufRY,21516
|
||||
oauthlib/oauth2/rfc6749/grant_types/base.py,sha256=uc1-nu-V6RvCvCQEMvrEj_exo00sxvuMCoNrYjdXBtg,7576
|
||||
oauthlib/oauth2/rfc6749/grant_types/client_credentials.py,sha256=mT9HnCy5HX9sJADK6cu9kr61Nl5mKIEeMKzhXfCUfAg,4948
|
||||
oauthlib/oauth2/rfc6749/grant_types/implicit.py,sha256=Pu2I-n4Qj66iowSvK8m23zYrXobRtaMx0ag_REGPKrw,17622
|
||||
oauthlib/oauth2/rfc6749/grant_types/openid_connect.py,sha256=GgcuURNoDYKihZ0xOidM8cciqwo9L6Alv21oAlxYBxY,18890
|
||||
oauthlib/oauth2/rfc6749/grant_types/refresh_token.py,sha256=8Y5zSOp_XOFnfTr7GX4B8WTHES6Bwml7qPFim0s8otY,5645
|
||||
oauthlib/oauth2/rfc6749/grant_types/resource_owner_password_credentials.py,sha256=WkW1Y3RbB1WMO-QizoPug0eMtsu8Ymdc2Q0U82sEwv8,8413
|
||||
oauthlib-2.0.6.dist-info/DESCRIPTION.rst,sha256=F1IdCzKfZjWQvAqGfqIkqQQ1dwIFRaR-Flq6KU0IdLo,4094
|
||||
oauthlib-2.0.6.dist-info/METADATA,sha256=MjWxacup6UJJnTGhvB2NglMRT8EzphRtmTYyhN5Gyl0,5793
|
||||
oauthlib-2.0.6.dist-info/RECORD,,
|
||||
oauthlib-2.0.6.dist-info/WHEEL,sha256=kdsN-5OJAZIiHN-iO4Rhl82KyS0bDWf4uBwMbkNafr8,110
|
||||
oauthlib-2.0.6.dist-info/metadata.json,sha256=BzRK_BaPs9KGVccmi9UQeTdIMt8Ho9IlDWdXzMAp03w,1714
|
||||
oauthlib-2.0.6.dist-info/top_level.txt,sha256=gz2py0fFs1AhG1O7KpHPcIXOgXOwdIiCaSnmLkiR12Q,9
|
||||
oauthlib-2.0.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
oauthlib/oauth2/rfc6749/clients/__pycache__/mobile_application.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/clients/__pycache__/legacy_application.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/clients/__pycache__/web_application.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/clients/__pycache__/base.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/clients/__pycache__/__init__.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/clients/__pycache__/service_application.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/clients/__pycache__/backend_application.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/endpoints/__pycache__/authorization.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/endpoints/__pycache__/resource.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/endpoints/__pycache__/token.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/endpoints/__pycache__/base.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/endpoints/__pycache__/revocation.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/endpoints/__pycache__/__init__.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/endpoints/__pycache__/pre_configured.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/__pycache__/tokens.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/__pycache__/request_validator.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/__pycache__/parameters.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/__pycache__/errors.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/__pycache__/utils.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/__pycache__/__init__.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/grant_types/__pycache__/refresh_token.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/grant_types/__pycache__/authorization_code.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/grant_types/__pycache__/client_credentials.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/grant_types/__pycache__/resource_owner_password_credentials.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/grant_types/__pycache__/base.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/grant_types/__pycache__/implicit.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/grant_types/__pycache__/openid_connect.cpython-36.pyc,,
|
||||
oauthlib/oauth2/rfc6749/grant_types/__pycache__/__init__.cpython-36.pyc,,
|
||||
oauthlib/oauth2/__pycache__/__init__.cpython-36.pyc,,
|
||||
oauthlib/__pycache__/signals.cpython-36.pyc,,
|
||||
oauthlib/__pycache__/common.cpython-36.pyc,,
|
||||
oauthlib/__pycache__/uri_validate.cpython-36.pyc,,
|
||||
oauthlib/__pycache__/__init__.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/endpoints/__pycache__/authorization.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/endpoints/__pycache__/signature_only.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/endpoints/__pycache__/resource.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/endpoints/__pycache__/request_token.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/endpoints/__pycache__/base.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/endpoints/__pycache__/__init__.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/endpoints/__pycache__/pre_configured.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/endpoints/__pycache__/access_token.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/__pycache__/request_validator.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/__pycache__/parameters.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/__pycache__/errors.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/__pycache__/utils.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/__pycache__/__init__.cpython-36.pyc,,
|
||||
oauthlib/oauth1/rfc5849/__pycache__/signature.cpython-36.pyc,,
|
||||
oauthlib/oauth1/__pycache__/__init__.cpython-36.pyc,,
|
||||
@@ -0,0 +1,6 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.30.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved", "License :: OSI Approved :: BSD License", "Operating System :: MacOS", "Operating System :: POSIX", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules"], "extensions": {"python.details": {"contacts": [{"email": "ib.lundgren@gmail.com", "name": "Ib Lundgren", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/idan/oauthlib"}}}, "extras": ["rsa", "signals", "signedtoken", "test"], "generator": "bdist_wheel (0.30.0)", "license": "BSD", "metadata_version": "2.0", "name": "oauthlib", "platform": "any", "run_requires": [{"extra": "signals", "requires": ["blinker"]}, {"extra": "test", "requires": ["blinker", "cryptography", "nose", "pyjwt (>=1.0.0)"]}, {"extra": "rsa", "requires": ["cryptography"]}, {"extra": "signedtoken", "requires": ["cryptography", "pyjwt (>=1.0.0)"]}], "summary": "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic", "test_requires": [{"requires": ["blinker", "cryptography", "nose", "pyjwt (>=1.0.0)"]}], "version": "2.0.6"}
|
||||
@@ -0,0 +1 @@
|
||||
oauthlib
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
oauthlib
|
||||
~~~~~~~~
|
||||
|
||||
A generic, spec-compliant, thorough implementation of the OAuth
|
||||
request-signing logic.
|
||||
|
||||
:copyright: (c) 2011 by Idan Gazit.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
__author__ = 'Idan Gazit <idan@gazit.me>'
|
||||
__version__ = '2.0.6'
|
||||
|
||||
|
||||
import logging
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
class NullHandler(logging.Handler):
|
||||
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
logging.getLogger('oauthlib').addHandler(NullHandler())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,460 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.common
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module provides data structures and utilities common
|
||||
to all implementations of OAuth.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
from urllib import quote as _quote
|
||||
from urllib import unquote as _unquote
|
||||
from urllib import urlencode as _urlencode
|
||||
except ImportError:
|
||||
from urllib.parse import quote as _quote
|
||||
from urllib.parse import unquote as _unquote
|
||||
from urllib.parse import urlencode as _urlencode
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
'0123456789')
|
||||
|
||||
CLIENT_ID_CHARACTER_SET = (r' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN'
|
||||
'OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}')
|
||||
|
||||
SANITIZE_PATTERN = re.compile(r'([^&;]*(?:password|token)[^=]*=)[^&;]+', re.IGNORECASE)
|
||||
INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]')
|
||||
|
||||
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
'abcdefghijklmnopqrstuvwxyz'
|
||||
'0123456789' '_.-')
|
||||
|
||||
log = logging.getLogger('oauthlib')
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3:
|
||||
unicode_type = str
|
||||
bytes_type = bytes
|
||||
else:
|
||||
unicode_type = unicode
|
||||
bytes_type = str
|
||||
|
||||
|
||||
# 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
|
||||
def quote(s, safe=b'/'):
|
||||
s = s.encode('utf-8') if isinstance(s, unicode_type) else s
|
||||
s = _quote(s, safe)
|
||||
# PY3 always returns unicode. PY2 may return either, depending on whether
|
||||
# it had to modify the string.
|
||||
if isinstance(s, bytes_type):
|
||||
s = s.decode('utf-8')
|
||||
return s
|
||||
|
||||
|
||||
def unquote(s):
|
||||
s = _unquote(s)
|
||||
# PY3 always returns unicode. PY2 seems to always return what you give it,
|
||||
# which differs from quote's behavior. Just to be safe, make sure it is
|
||||
# unicode before we return.
|
||||
if isinstance(s, bytes_type):
|
||||
s = s.decode('utf-8')
|
||||
return s
|
||||
|
||||
|
||||
def urlencode(params):
|
||||
utf8_params = encode_params_utf8(params)
|
||||
urlencoded = _urlencode(utf8_params)
|
||||
if isinstance(urlencoded, unicode_type): # PY3 returns unicode
|
||||
return urlencoded
|
||||
else:
|
||||
return urlencoded.decode("utf-8")
|
||||
|
||||
|
||||
def encode_params_utf8(params):
|
||||
"""Ensures that all parameters in a list of 2-element tuples are encoded to
|
||||
bytestrings using UTF-8
|
||||
"""
|
||||
encoded = []
|
||||
for k, v in params:
|
||||
encoded.append((
|
||||
k.encode('utf-8') if isinstance(k, unicode_type) else k,
|
||||
v.encode('utf-8') if isinstance(v, unicode_type) else v))
|
||||
return encoded
|
||||
|
||||
|
||||
def decode_params_utf8(params):
|
||||
"""Ensures that all parameters in a list of 2-element tuples are decoded to
|
||||
unicode using UTF-8.
|
||||
"""
|
||||
decoded = []
|
||||
for k, v in params:
|
||||
decoded.append((
|
||||
k.decode('utf-8') if isinstance(k, bytes_type) else k,
|
||||
v.decode('utf-8') if isinstance(v, bytes_type) else v))
|
||||
return decoded
|
||||
|
||||
|
||||
urlencoded = set(always_safe) | set('=&;:%+~,*@!()/?')
|
||||
|
||||
|
||||
def urldecode(query):
|
||||
"""Decode a query string in x-www-form-urlencoded format into a sequence
|
||||
of two-element tuples.
|
||||
|
||||
Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
|
||||
correct formatting of the query string by validation. If validation fails
|
||||
a ValueError will be raised. urllib.parse_qsl will only raise errors if
|
||||
any of name-value pairs omits the equals sign.
|
||||
"""
|
||||
# Check if query contains invalid characters
|
||||
if query and not set(query) <= urlencoded:
|
||||
error = ("Error trying to decode a non urlencoded string. "
|
||||
"Found invalid characters: %s "
|
||||
"in the string: '%s'. "
|
||||
"Please ensure the request/response body is "
|
||||
"x-www-form-urlencoded.")
|
||||
raise ValueError(error % (set(query) - urlencoded, query))
|
||||
|
||||
# Check for correctly hex encoded values using a regular expression
|
||||
# All encoded values begin with % followed by two hex characters
|
||||
# correct = %00, %A0, %0A, %FF
|
||||
# invalid = %G0, %5H, %PO
|
||||
if INVALID_HEX_PATTERN.search(query):
|
||||
raise ValueError('Invalid hex encoding in query string.')
|
||||
|
||||
# We encode to utf-8 prior to parsing because parse_qsl behaves
|
||||
# differently on unicode input in python 2 and 3.
|
||||
# Python 2.7
|
||||
# >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6')
|
||||
# u'\xe5\x95\xa6\xe5\x95\xa6'
|
||||
# Python 2.7, non unicode input gives the same
|
||||
# >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6')
|
||||
# '\xe5\x95\xa6\xe5\x95\xa6'
|
||||
# but now we can decode it to unicode
|
||||
# >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8')
|
||||
# u'\u5566\u5566'
|
||||
# Python 3.3 however
|
||||
# >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6')
|
||||
# u'\u5566\u5566'
|
||||
query = query.encode(
|
||||
'utf-8') if not PY3 and isinstance(query, unicode_type) else query
|
||||
# We want to allow queries such as "c2" whereas urlparse.parse_qsl
|
||||
# with the strict_parsing flag will not.
|
||||
params = urlparse.parse_qsl(query, keep_blank_values=True)
|
||||
|
||||
# unicode all the things
|
||||
return decode_params_utf8(params)
|
||||
|
||||
|
||||
def extract_params(raw):
|
||||
"""Extract parameters and return them as a list of 2-tuples.
|
||||
|
||||
Will successfully extract parameters from urlencoded query strings,
|
||||
dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
|
||||
empty list of parameters. Any other input will result in a return
|
||||
value of None.
|
||||
"""
|
||||
if isinstance(raw, bytes_type) or isinstance(raw, unicode_type):
|
||||
try:
|
||||
params = urldecode(raw)
|
||||
except ValueError:
|
||||
params = None
|
||||
elif hasattr(raw, '__iter__'):
|
||||
try:
|
||||
dict(raw)
|
||||
except ValueError:
|
||||
params = None
|
||||
except TypeError:
|
||||
params = None
|
||||
else:
|
||||
params = list(raw.items() if isinstance(raw, dict) else raw)
|
||||
params = decode_params_utf8(params)
|
||||
else:
|
||||
params = None
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def generate_nonce():
|
||||
"""Generate pseudorandom nonce that is unlikely to repeat.
|
||||
|
||||
Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
|
||||
Per `section 3.2.1`_ of the MAC Access Authentication spec.
|
||||
|
||||
A random 64-bit number is appended to the epoch timestamp for both
|
||||
randomness and to decrease the likelihood of collisions.
|
||||
|
||||
.. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
|
||||
.. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
|
||||
"""
|
||||
return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp())
|
||||
|
||||
|
||||
def generate_timestamp():
|
||||
"""Get seconds since epoch (UTC).
|
||||
|
||||
Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
|
||||
Per `section 3.2.1`_ of the MAC Access Authentication spec.
|
||||
|
||||
.. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
|
||||
.. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
|
||||
"""
|
||||
return unicode_type(int(time.time()))
|
||||
|
||||
|
||||
def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
|
||||
"""Generates a non-guessable OAuth token
|
||||
|
||||
OAuth (1 and 2) does not specify the format of tokens except that they
|
||||
should be strings of random characters. Tokens should not be guessable
|
||||
and entropy when generating the random characters is important. Which is
|
||||
why SystemRandom is used instead of the default random.choice method.
|
||||
"""
|
||||
rand = random.SystemRandom()
|
||||
return ''.join(rand.choice(chars) for x in range(length))
|
||||
|
||||
|
||||
def generate_signed_token(private_pem, request):
|
||||
import jwt
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
|
||||
claims = {
|
||||
'scope': request.scope,
|
||||
'exp': now + datetime.timedelta(seconds=request.expires_in)
|
||||
}
|
||||
|
||||
claims.update(request.claims)
|
||||
|
||||
token = jwt.encode(claims, private_pem, 'RS256')
|
||||
token = to_unicode(token, "UTF-8")
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def verify_signed_token(public_pem, token):
|
||||
import jwt
|
||||
|
||||
return jwt.decode(token, public_pem, algorithms=['RS256'])
|
||||
|
||||
|
||||
def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
|
||||
"""Generates an OAuth client_id
|
||||
|
||||
OAuth 2 specify the format of client_id in
|
||||
http://tools.ietf.org/html/rfc6749#appendix-A.
|
||||
"""
|
||||
return generate_token(length, chars)
|
||||
|
||||
|
||||
def add_params_to_qs(query, params):
|
||||
"""Extend a query with a list of two-tuples."""
|
||||
if isinstance(params, dict):
|
||||
params = params.items()
|
||||
queryparams = urlparse.parse_qsl(query, keep_blank_values=True)
|
||||
queryparams.extend(params)
|
||||
return urlencode(queryparams)
|
||||
|
||||
|
||||
def add_params_to_uri(uri, params, fragment=False):
|
||||
"""Add a list of two-tuples to the uri query components."""
|
||||
sch, net, path, par, query, fra = urlparse.urlparse(uri)
|
||||
if fragment:
|
||||
fra = add_params_to_qs(fra, params)
|
||||
else:
|
||||
query = add_params_to_qs(query, params)
|
||||
return urlparse.urlunparse((sch, net, path, par, query, fra))
|
||||
|
||||
|
||||
def safe_string_equals(a, b):
|
||||
""" Near-constant time string comparison.
|
||||
|
||||
Used in order to avoid timing attacks on sensitive information such
|
||||
as secret keys during request verification (`rootLabs`_).
|
||||
|
||||
.. _`rootLabs`: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
|
||||
|
||||
"""
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
|
||||
result = 0
|
||||
for x, y in zip(a, b):
|
||||
result |= ord(x) ^ ord(y)
|
||||
return result == 0
|
||||
|
||||
|
||||
def to_unicode(data, encoding='UTF-8'):
|
||||
"""Convert a number of different types of objects to unicode."""
|
||||
if isinstance(data, unicode_type):
|
||||
return data
|
||||
|
||||
if isinstance(data, bytes_type):
|
||||
return unicode_type(data, encoding=encoding)
|
||||
|
||||
if hasattr(data, '__iter__'):
|
||||
try:
|
||||
dict(data)
|
||||
except TypeError:
|
||||
pass
|
||||
except ValueError:
|
||||
# Assume it's a one dimensional data structure
|
||||
return (to_unicode(i, encoding) for i in data)
|
||||
else:
|
||||
# We support 2.6 which lacks dict comprehensions
|
||||
if hasattr(data, 'items'):
|
||||
data = data.items()
|
||||
return dict(((to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class CaseInsensitiveDict(dict):
|
||||
|
||||
"""Basic case insensitive dict with strings only keys."""
|
||||
|
||||
proxy = {}
|
||||
|
||||
def __init__(self, data):
|
||||
self.proxy = dict((k.lower(), k) for k in data)
|
||||
for k in data:
|
||||
self[k] = data[k]
|
||||
|
||||
def __contains__(self, k):
|
||||
return k.lower() in self.proxy
|
||||
|
||||
def __delitem__(self, k):
|
||||
key = self.proxy[k.lower()]
|
||||
super(CaseInsensitiveDict, self).__delitem__(key)
|
||||
del self.proxy[k.lower()]
|
||||
|
||||
def __getitem__(self, k):
|
||||
key = self.proxy[k.lower()]
|
||||
return super(CaseInsensitiveDict, self).__getitem__(key)
|
||||
|
||||
def get(self, k, default=None):
|
||||
return self[k] if k in self else default
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
super(CaseInsensitiveDict, self).__setitem__(k, v)
|
||||
self.proxy[k.lower()] = k
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
super(CaseInsensitiveDict, self).update(*args, **kwargs)
|
||||
for k in dict(*args, **kwargs):
|
||||
self.proxy[k.lower()] = k
|
||||
|
||||
|
||||
class Request(object):
|
||||
|
||||
"""A malleable representation of a signable HTTP request.
|
||||
|
||||
Body argument may contain any data, but parameters will only be decoded if
|
||||
they are one of:
|
||||
|
||||
* urlencoded query string
|
||||
* dict
|
||||
* list of 2-tuples
|
||||
|
||||
Anything else will be treated as raw body data to be passed through
|
||||
unmolested.
|
||||
"""
|
||||
|
||||
def __init__(self, uri, http_method='GET', body=None, headers=None,
|
||||
encoding='utf-8'):
|
||||
# Convert to unicode using encoding if given, else assume unicode
|
||||
encode = lambda x: to_unicode(x, encoding) if encoding else x
|
||||
|
||||
self.uri = encode(uri)
|
||||
self.http_method = encode(http_method)
|
||||
self.headers = CaseInsensitiveDict(encode(headers or {}))
|
||||
self.body = encode(body)
|
||||
self.decoded_body = extract_params(self.body)
|
||||
self.oauth_params = []
|
||||
self.validator_log = {}
|
||||
|
||||
self._params = {
|
||||
"access_token": None,
|
||||
"client": None,
|
||||
"client_id": None,
|
||||
"client_secret": None,
|
||||
"code": None,
|
||||
"extra_credentials": None,
|
||||
"grant_type": None,
|
||||
"redirect_uri": None,
|
||||
"refresh_token": None,
|
||||
"request_token": None,
|
||||
"response_type": None,
|
||||
"scope": None,
|
||||
"scopes": None,
|
||||
"state": None,
|
||||
"token": None,
|
||||
"user": None,
|
||||
"token_type_hint": None,
|
||||
|
||||
# OpenID Connect
|
||||
"response_mode": None,
|
||||
"nonce": None,
|
||||
"display": None,
|
||||
"prompt": None,
|
||||
"claims": None,
|
||||
"max_age": None,
|
||||
"ui_locales": None,
|
||||
"id_token_hint": None,
|
||||
"login_hint": None,
|
||||
"acr_values": None
|
||||
}
|
||||
self._params.update(dict(urldecode(self.uri_query)))
|
||||
self._params.update(dict(self.decoded_body or []))
|
||||
self._params.update(self.headers)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self._params:
|
||||
return self._params[name]
|
||||
else:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __repr__(self):
|
||||
body = self.body
|
||||
headers = self.headers.copy()
|
||||
if body:
|
||||
body = SANITIZE_PATTERN.sub('\1<SANITIZED>', str(body))
|
||||
if 'Authorization' in headers:
|
||||
headers['Authorization'] = '<SANITIZED>'
|
||||
return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
|
||||
self.uri, self.http_method, headers, body)
|
||||
|
||||
@property
|
||||
def uri_query(self):
|
||||
return urlparse.urlparse(self.uri).query
|
||||
|
||||
@property
|
||||
def uri_query_params(self):
|
||||
if not self.uri_query:
|
||||
return []
|
||||
return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
|
||||
strict_parsing=True)
|
||||
|
||||
@property
|
||||
def duplicate_params(self):
|
||||
seen_keys = collections.defaultdict(int)
|
||||
all_keys = (p[0]
|
||||
for p in (self.decoded_body or []) + self.uri_query_params)
|
||||
for k in all_keys:
|
||||
seen_keys[k] += 1
|
||||
return [k for k, c in seen_keys.items() if c > 1]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth1
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is a wrapper for the most recent implementation of OAuth 1.0 Client
|
||||
and Server classes.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from .rfc5849 import Client
|
||||
from .rfc5849 import SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT
|
||||
from .rfc5849 import SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_QUERY
|
||||
from .rfc5849 import SIGNATURE_TYPE_BODY
|
||||
from .rfc5849.request_validator import RequestValidator
|
||||
from .rfc5849.endpoints import RequestTokenEndpoint, AuthorizationEndpoint
|
||||
from .rfc5849.endpoints import AccessTokenEndpoint, ResourceEndpoint
|
||||
from .rfc5849.endpoints import SignatureOnlyEndpoint, WebApplicationServer
|
||||
from .rfc5849.errors import InsecureTransportError, InvalidClientError, InvalidRequestError, InvalidSignatureMethodError, OAuth1Error
|
||||
Binary file not shown.
@@ -0,0 +1,328 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for signing and checking OAuth 1.0 RFC 5849 requests.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
import sys
|
||||
try:
|
||||
import urlparse
|
||||
except ImportError:
|
||||
import urllib.parse as urlparse
|
||||
|
||||
if sys.version_info[0] == 3:
|
||||
bytes_type = bytes
|
||||
else:
|
||||
bytes_type = str
|
||||
|
||||
from oauthlib.common import Request, urlencode, generate_nonce
|
||||
from oauthlib.common import generate_timestamp, to_unicode
|
||||
from . import parameters, signature
|
||||
|
||||
SIGNATURE_HMAC = "HMAC-SHA1"
|
||||
SIGNATURE_RSA = "RSA-SHA1"
|
||||
SIGNATURE_PLAINTEXT = "PLAINTEXT"
|
||||
SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
|
||||
|
||||
SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
|
||||
SIGNATURE_TYPE_QUERY = 'QUERY'
|
||||
SIGNATURE_TYPE_BODY = 'BODY'
|
||||
|
||||
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
"""A client used to sign OAuth 1.0 RFC 5849 requests."""
|
||||
SIGNATURE_METHODS = {
|
||||
SIGNATURE_HMAC: signature.sign_hmac_sha1_with_client,
|
||||
SIGNATURE_RSA: signature.sign_rsa_sha1_with_client,
|
||||
SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def register_signature_method(cls, method_name, method_callback):
|
||||
cls.SIGNATURE_METHODS[method_name] = method_callback
|
||||
|
||||
def __init__(self, client_key,
|
||||
client_secret=None,
|
||||
resource_owner_key=None,
|
||||
resource_owner_secret=None,
|
||||
callback_uri=None,
|
||||
signature_method=SIGNATURE_HMAC,
|
||||
signature_type=SIGNATURE_TYPE_AUTH_HEADER,
|
||||
rsa_key=None, verifier=None, realm=None,
|
||||
encoding='utf-8', decoding=None,
|
||||
nonce=None, timestamp=None):
|
||||
"""Create an OAuth 1 client.
|
||||
|
||||
:param client_key: Client key (consumer key), mandatory.
|
||||
:param resource_owner_key: Resource owner key (oauth token).
|
||||
:param resource_owner_secret: Resource owner secret (oauth token secret).
|
||||
:param callback_uri: Callback used when obtaining request token.
|
||||
:param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT.
|
||||
:param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default),
|
||||
SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY
|
||||
depending on where you want to embed the oauth
|
||||
credentials.
|
||||
:param rsa_key: RSA key used with SIGNATURE_RSA.
|
||||
:param verifier: Verifier used when obtaining an access token.
|
||||
:param realm: Realm (scope) to which access is being requested.
|
||||
:param encoding: If you provide non-unicode input you may use this
|
||||
to have oauthlib automatically convert.
|
||||
:param decoding: If you wish that the returned uri, headers and body
|
||||
from sign be encoded back from unicode, then set
|
||||
decoding to your preferred encoding, i.e. utf-8.
|
||||
:param nonce: Use this nonce instead of generating one. (Mainly for testing)
|
||||
:param timestamp: Use this timestamp instead of using current. (Mainly for testing)
|
||||
"""
|
||||
# Convert to unicode using encoding if given, else assume unicode
|
||||
encode = lambda x: to_unicode(x, encoding) if encoding else x
|
||||
|
||||
self.client_key = encode(client_key)
|
||||
self.client_secret = encode(client_secret)
|
||||
self.resource_owner_key = encode(resource_owner_key)
|
||||
self.resource_owner_secret = encode(resource_owner_secret)
|
||||
self.signature_method = encode(signature_method)
|
||||
self.signature_type = encode(signature_type)
|
||||
self.callback_uri = encode(callback_uri)
|
||||
self.rsa_key = encode(rsa_key)
|
||||
self.verifier = encode(verifier)
|
||||
self.realm = encode(realm)
|
||||
self.encoding = encode(encoding)
|
||||
self.decoding = encode(decoding)
|
||||
self.nonce = encode(nonce)
|
||||
self.timestamp = encode(timestamp)
|
||||
|
||||
def __repr__(self):
|
||||
attrs = vars(self).copy()
|
||||
attrs['client_secret'] = '****' if attrs['client_secret'] else None
|
||||
attrs['rsa_key'] = '****' if attrs['rsa_key'] else None
|
||||
attrs[
|
||||
'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None
|
||||
attribute_str = ', '.join('%s=%s' % (k, v) for k, v in attrs.items())
|
||||
return '<%s %s>' % (self.__class__.__name__, attribute_str)
|
||||
|
||||
def get_oauth_signature(self, request):
|
||||
"""Get an OAuth signature to be used in signing a request
|
||||
|
||||
To satisfy `section 3.4.1.2`_ item 2, if the request argument's
|
||||
headers dict attribute contains a Host item, its value will
|
||||
replace any netloc part of the request argument's uri attribute
|
||||
value.
|
||||
|
||||
.. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
||||
"""
|
||||
if self.signature_method == SIGNATURE_PLAINTEXT:
|
||||
# fast-path
|
||||
return signature.sign_plaintext(self.client_secret,
|
||||
self.resource_owner_secret)
|
||||
|
||||
uri, headers, body = self._render(request)
|
||||
|
||||
collected_params = signature.collect_parameters(
|
||||
uri_query=urlparse.urlparse(uri).query,
|
||||
body=body,
|
||||
headers=headers)
|
||||
log.debug("Collected params: {0}".format(collected_params))
|
||||
|
||||
normalized_params = signature.normalize_parameters(collected_params)
|
||||
normalized_uri = signature.normalize_base_string_uri(uri,
|
||||
headers.get('Host', None))
|
||||
log.debug("Normalized params: {0}".format(normalized_params))
|
||||
log.debug("Normalized URI: {0}".format(normalized_uri))
|
||||
|
||||
base_string = signature.construct_base_string(request.http_method,
|
||||
normalized_uri, normalized_params)
|
||||
|
||||
log.debug("Signing: signature base string: {0}".format(base_string))
|
||||
|
||||
if self.signature_method not in self.SIGNATURE_METHODS:
|
||||
raise ValueError('Invalid signature method.')
|
||||
|
||||
sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
|
||||
|
||||
log.debug("Signature: {0}".format(sig))
|
||||
return sig
|
||||
|
||||
def get_oauth_params(self, request):
|
||||
"""Get the basic OAuth parameters to be used in generating a signature.
|
||||
"""
|
||||
nonce = (generate_nonce()
|
||||
if self.nonce is None else self.nonce)
|
||||
timestamp = (generate_timestamp()
|
||||
if self.timestamp is None else self.timestamp)
|
||||
params = [
|
||||
('oauth_nonce', nonce),
|
||||
('oauth_timestamp', timestamp),
|
||||
('oauth_version', '1.0'),
|
||||
('oauth_signature_method', self.signature_method),
|
||||
('oauth_consumer_key', self.client_key),
|
||||
]
|
||||
if self.resource_owner_key:
|
||||
params.append(('oauth_token', self.resource_owner_key))
|
||||
if self.callback_uri:
|
||||
params.append(('oauth_callback', self.callback_uri))
|
||||
if self.verifier:
|
||||
params.append(('oauth_verifier', self.verifier))
|
||||
|
||||
# providing body hash for requests other than x-www-form-urlencoded
|
||||
# as described in http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
|
||||
# 4.1.1. When to include the body hash
|
||||
# * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
|
||||
# * [...] SHOULD include the oauth_body_hash parameter on all other requests.
|
||||
content_type = request.headers.get('Content-Type', None)
|
||||
content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
|
||||
if request.body is not None and content_type_eligible:
|
||||
params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))
|
||||
|
||||
return params
|
||||
|
||||
def _render(self, request, formencode=False, realm=None):
|
||||
"""Render a signed request according to signature type
|
||||
|
||||
Returns a 3-tuple containing the request URI, headers, and body.
|
||||
|
||||
If the formencode argument is True and the body contains parameters, it
|
||||
is escaped and returned as a valid formencoded string.
|
||||
"""
|
||||
# TODO what if there are body params on a header-type auth?
|
||||
# TODO what if there are query params on a body-type auth?
|
||||
|
||||
uri, headers, body = request.uri, request.headers, request.body
|
||||
|
||||
# TODO: right now these prepare_* methods are very narrow in scope--they
|
||||
# only affect their little thing. In some cases (for example, with
|
||||
# header auth) it might be advantageous to allow these methods to touch
|
||||
# other parts of the request, like the headers—so the prepare_headers
|
||||
# method could also set the Content-Type header to x-www-form-urlencoded
|
||||
# like the spec requires. This would be a fundamental change though, and
|
||||
# I'm not sure how I feel about it.
|
||||
if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
|
||||
headers = parameters.prepare_headers(
|
||||
request.oauth_params, request.headers, realm=realm)
|
||||
elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None:
|
||||
body = parameters.prepare_form_encoded_body(
|
||||
request.oauth_params, request.decoded_body)
|
||||
if formencode:
|
||||
body = urlencode(body)
|
||||
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
elif self.signature_type == SIGNATURE_TYPE_QUERY:
|
||||
uri = parameters.prepare_request_uri_query(
|
||||
request.oauth_params, request.uri)
|
||||
else:
|
||||
raise ValueError('Unknown signature type specified.')
|
||||
|
||||
return uri, headers, body
|
||||
|
||||
def sign(self, uri, http_method='GET', body=None, headers=None, realm=None):
|
||||
"""Sign a request
|
||||
|
||||
Signs an HTTP request with the specified parts.
|
||||
|
||||
Returns a 3-tuple of the signed request's URI, headers, and body.
|
||||
Note that http_method is not returned as it is unaffected by the OAuth
|
||||
signing process. Also worth noting is that duplicate parameters
|
||||
will be included in the signature, regardless of where they are
|
||||
specified (query, body).
|
||||
|
||||
The body argument may be a dict, a list of 2-tuples, or a formencoded
|
||||
string. The Content-Type header must be 'application/x-www-form-urlencoded'
|
||||
if it is present.
|
||||
|
||||
If the body argument is not one of the above, it will be returned
|
||||
verbatim as it is unaffected by the OAuth signing process. Attempting to
|
||||
sign a request with non-formencoded data using the OAuth body signature
|
||||
type is invalid and will raise an exception.
|
||||
|
||||
If the body does contain parameters, it will be returned as a properly-
|
||||
formatted formencoded string.
|
||||
|
||||
Body may not be included if the http_method is either GET or HEAD as
|
||||
this changes the semantic meaning of the request.
|
||||
|
||||
All string data MUST be unicode or be encoded with the same encoding
|
||||
scheme supplied to the Client constructor, default utf-8. This includes
|
||||
strings inside body dicts, for example.
|
||||
"""
|
||||
# normalize request data
|
||||
request = Request(uri, http_method, body, headers,
|
||||
encoding=self.encoding)
|
||||
|
||||
# sanity check
|
||||
content_type = request.headers.get('Content-Type', None)
|
||||
multipart = content_type and content_type.startswith('multipart/')
|
||||
should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED
|
||||
has_params = request.decoded_body is not None
|
||||
# 3.4.1.3.1. Parameter Sources
|
||||
# [Parameters are collected from the HTTP request entity-body, but only
|
||||
# if [...]:
|
||||
# * The entity-body is single-part.
|
||||
if multipart and has_params:
|
||||
raise ValueError(
|
||||
"Headers indicate a multipart body but body contains parameters.")
|
||||
# * The entity-body follows the encoding requirements of the
|
||||
# "application/x-www-form-urlencoded" content-type as defined by
|
||||
# [W3C.REC-html40-19980424].
|
||||
elif should_have_params and not has_params:
|
||||
raise ValueError(
|
||||
"Headers indicate a formencoded body but body was not decodable.")
|
||||
# * The HTTP request entity-header includes the "Content-Type"
|
||||
# header field set to "application/x-www-form-urlencoded".
|
||||
elif not should_have_params and has_params:
|
||||
raise ValueError(
|
||||
"Body contains parameters but Content-Type header was {0} "
|
||||
"instead of {1}".format(content_type or "not set",
|
||||
CONTENT_TYPE_FORM_URLENCODED))
|
||||
|
||||
# 3.5.2. Form-Encoded Body
|
||||
# Protocol parameters can be transmitted in the HTTP request entity-
|
||||
# body, but only if the following REQUIRED conditions are met:
|
||||
# o The entity-body is single-part.
|
||||
# o The entity-body follows the encoding requirements of the
|
||||
# "application/x-www-form-urlencoded" content-type as defined by
|
||||
# [W3C.REC-html40-19980424].
|
||||
# o The HTTP request entity-header includes the "Content-Type" header
|
||||
# field set to "application/x-www-form-urlencoded".
|
||||
elif self.signature_type == SIGNATURE_TYPE_BODY and not (
|
||||
should_have_params and has_params and not multipart):
|
||||
raise ValueError(
|
||||
'Body signatures may only be used with form-urlencoded content')
|
||||
|
||||
# We amend http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
|
||||
# with the clause that parameters from body should only be included
|
||||
# in non GET or HEAD requests. Extracting the request body parameters
|
||||
# and including them in the signature base string would give semantic
|
||||
# meaning to the body, which it should not have according to the
|
||||
# HTTP 1.1 spec.
|
||||
elif http_method.upper() in ('GET', 'HEAD') and has_params:
|
||||
raise ValueError('GET/HEAD requests should not include body.')
|
||||
|
||||
# generate the basic OAuth parameters
|
||||
request.oauth_params = self.get_oauth_params(request)
|
||||
|
||||
# generate the signature
|
||||
request.oauth_params.append(
|
||||
('oauth_signature', self.get_oauth_signature(request)))
|
||||
|
||||
# render the signed request and return it
|
||||
uri, headers, body = self._render(request, formencode=True,
|
||||
realm=(realm or self.realm))
|
||||
|
||||
if self.decoding:
|
||||
log.debug('Encoding URI, headers and body to %s.', self.decoding)
|
||||
uri = uri.encode(self.decoding)
|
||||
body = body.encode(self.decoding) if body else body
|
||||
new_headers = {}
|
||||
for k, v in headers.items():
|
||||
new_headers[k.encode(self.decoding)] = v.encode(self.decoding)
|
||||
headers = new_headers
|
||||
return uri, headers, body
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user