Skip to content

Commit

Permalink
Add Sri v2.0 (#42)
Browse files Browse the repository at this point in the history
* implements sri + adds unit tests

* implements sri + adds unit tests

* Update README.md

Add a description of how to use Sri.

* adds possibility to disable sri for default version

* Update README.md
  • Loading branch information
M0r13n authored and miguelgrinberg committed May 19, 2019
1 parent db85c55 commit d87eaf8
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 12 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ A timestamp created in this way is an HTML string that can be returned as part o

The Ajax callback in the browser needs to call `flask_moment_render_all()` each time an element containing a timestamp is added to the DOM. The included application demonstrates how this is done.

Subresource Integrity(SRI)
-----------
[SRI ](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) is a security feature that enables browsers to verify that resources they fetch are not maliciously manipulated. To do so a cryptographic hash is provided that proves integrity.

SRI is enabled by default. If you wish to use another version or want to host your own javascript, a [separate hash ](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity#Tools_for_generating_SRI_hashes) can be provided.
Just add `sri=<YOUR-HASH>` when calling either `moment.include_moment()` or `moment.include_jquery()`. If no sri hash is provided and you choose to use a non default version of javascript, no sri hash will be added.

You can always choose to disable sri. To do so just set `sri=False`.


Development
-----------

Expand Down
45 changes: 37 additions & 8 deletions flask_moment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,33 @@

class _moment(object):
@staticmethod
def include_moment(version='2.18.1', local_js=None, no_js=None):
def include_moment(version='2.18.1', local_js=None, no_js=None, sri=None):
js = ''
if version == '2.18.1' and local_js is None and sri is None:
sri = ('sha384-iMhq1oHAQWG7+cVzHBvYynTbGZy'
'O4DniLR7bhY1Q39AMn8ePTV9uByV/06g2xqOS')
if not no_js:
if local_js is not None:
js = '<script src="%s"></script>\n' % local_js
if not sri:
js = '<script src="%s"></script>\n' % local_js
else:
js = ('<script src="%s" integrity="%s" '
'crossorigin="anonymous"></script>\n'
% (local_js, sri))
elif version is not None:
js_filename = 'moment-with-locales.min.js' \
if StrictVersion(version) >= StrictVersion('2.8.0') \
else 'moment-with-langs.min.js'
js = '<script src="//cdnjs.cloudflare.com/ajax/libs/' \
'moment.js/%s/%s"></script>\n' % (version, js_filename)
if not sri:
js = '<script src="//cdnjs.cloudflare.com/ajax/libs/' \
'moment.js/%s/%s"></script>\n' \
% (version, js_filename)
else:
js = '<script src="//cdnjs.cloudflare.com/ajax/libs/' \
'moment.js/%s/%s" integrity="%s" ' \
'crossorigin="anonymous"></script>\n' \
% (version, js_filename, sri)

return Markup('''%s<script>
moment.locale("en");
function flask_moment_render(elem) {
Expand All @@ -37,13 +53,26 @@ def include_moment(version='2.18.1', local_js=None, no_js=None):
</script>''' % js) # noqa: E501

@staticmethod
def include_jquery(version='2.1.0', local_js=None):
def include_jquery(version='2.1.0', local_js=None, sri=None):
js = ''
if sri is None and version == '2.1.0' and local_js is None:
sri = ('sha384-85/BFduEdDxQ86xztyNu4BBkVZmlv'
'u+iB7zhBu0VoYdq+ODs3PKpU6iVE3ZqPMut')
if local_js is not None:
js = '<script src="%s"></script>\n' % local_js
if not sri:
js = '<script src="%s"></script>\n' % local_js
else:
js = ('<script src="%s" integrity="%s" '
'crossorigin="anonymous"></script>\n' % (local_js, sri))

else:
js = ('<script src="//code.jquery.com/' +
'jquery-%s.min.js"></script>') % version
if not sri:
js = ('<script src="//code.jquery.com/' +
'jquery-%s.min.js"></script>') % version
else:
js = ('<script src="//code.jquery.com/jquery-%s.min.js" '
'integrity="%s" crossorigin="anonymous"></script>'
% (version, sri))
return Markup(js)

@staticmethod
Expand Down
194 changes: 190 additions & 4 deletions tests/test_flask_moment.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
from datetime import datetime

from flask import render_template_string
from flask_moment import _moment, Moment
from jinja2 import Markup
import hashlib
import base64
import re

# Python 2 and 3 compatibility
try:
import urllib.request as request
except ImportError:
import urllib as request


# Mock Objects

class NewDate(datetime):
"""http://stackoverflow.com/questions/4481954"""

@classmethod
def utcnow(cls):
return cls(2017, 1, 15, 22, 1, 21, 101361)
Expand All @@ -19,6 +28,7 @@ def utcnow(cls):

class NewPrivateMoment(_moment):
"""Mock the _moment class for predictable now timestamps"""

def __init__(self, timestamp=None, local=False):
if timestamp is None:
timestamp = _datetime_mock.utcnow()
Expand All @@ -31,6 +41,7 @@ def __init__(self, timestamp=None, local=False):

class NewPublicMoment(Moment):
"""Mock the Moment class for predictable now timestamps"""

def init_app(self, app):
if not hasattr(app, 'extensions'):
app.extensions = {}
Expand All @@ -50,7 +61,7 @@ def test_init_app(self, app, moment):

def test_app_context_processor(self, app, moment):
assert app.template_context_processors[None][1].__globals__[
'__name__'] == 'flask_moment'
'__name__'] == 'flask_moment'


class TestFlaskMomentIncludes(object):
Expand Down Expand Up @@ -146,7 +157,8 @@ def test__render_default(self):
assert isinstance(rts, Markup)
assert rts.find("thisisnotinthemarkup") < 0
assert rts.find("\"format\"") > 0
assert rts.find("data-refresh=\""+str(int(refresh)*60000)+"\"") > 0
assert rts.find(
"data-refresh=\"" + str(int(refresh) * 60000) + "\"") > 0

def test__render_refresh(self):
mom = _moment_mock()
Expand All @@ -156,7 +168,8 @@ def test__render_refresh(self):
assert isinstance(rts, Markup)
assert not rts.find("thisisnotinthemarkup") > 0
assert rts.find("\"format\"") > 0
assert rts.find("data-refresh=\""+str(int(refresh)*60000)+"\"") > 0
assert rts.find(
"data-refresh=\"" + str(int(refresh) * 60000) + "\"") > 0

def test_format_default(self):
mom = _moment_mock()
Expand Down Expand Up @@ -221,6 +234,7 @@ def test_unix_default(self):

class TestPublicMomentClass(object):
'''Public refers to the Moment class'''

def test_create_default_no_timestamp(self, app):
moment = _Moment()
moment.init_app(app)
Expand All @@ -234,3 +248,175 @@ def test_create_default_with_timestamp(self, app):
ts = datetime(2017, 1, 15, 22, 47, 6, 479898)

assert moment.create(timestamp=ts).timestamp == ts


class TestSubresourceIntegrity(object):
def test_jquery_with_non_default_version(self):
include_jquery = _moment.include_jquery(version='2.0.9')

assert 'src=\"' in include_jquery
assert 'integrity=\"' not in include_jquery
assert 'crossorigin=\"' not in include_jquery

def test_jquery_with_default_version(self):
include_jquery = _moment.include_jquery()

assert 'src=\"' in include_jquery
assert 'integrity=\"sha384' in include_jquery
assert 'crossorigin=\"anonymous\"' in include_jquery

def test_jquery_from_cdn_without_custom_sri_hash(self):
include_jquery = _moment.include_jquery(version='2.1.1',
sri='sha384-12345678')

assert ('<script src=\"//code.jquery.com/jquery-2.1.1.min.js\"'
' integrity=\"sha384-12345678\" crossorigin=\"anonymous\">'
'</script>') == include_jquery

def test_jquery_local_has_no_sri_as_default(self):
include_jquery = _moment.include_jquery(local_js=True)

assert 'src=\"' in include_jquery
assert 'integrity=\"' not in include_jquery
assert 'crossorigin\"' not in include_jquery

def test_jquery_local_with_sri(self):
include_jquery = _moment.include_jquery(local_js=True,
sri='sha384-12345678')

assert ('<script src=\"True\" integrity=\"sha384-12345678\"'
' crossorigin=\"anonymous\"></script>\n') == include_jquery

def test_disabling_sri_jquery_default(self):
include_jquery = _moment.include_jquery(sri=False)

assert 'src=\"' in include_jquery
assert 'integrity=\"' not in include_jquery
assert 'crossorigin\"' not in include_jquery

def test_disabling_sri_jquery_custom_js(self):
include_jquery = _moment.include_jquery(local_js=True, sri=False)

assert 'src=\"' in include_jquery
assert 'integrity=\"' not in include_jquery
assert 'crossorigin\"' not in include_jquery

def test_disabling_sri_jquery_custom_version(self):
include_jquery = _moment.include_jquery(version='2.1.1', sri=False)

assert 'src=\"' in include_jquery
assert 'integrity=\"' not in include_jquery
assert 'crossorigin\"' not in include_jquery

def test_moment_with_non_default_versions(self):
include_moment = None

def _check_assertions():
assert 'src=\"' in include_moment
assert 'integrity=\"' not in include_moment
assert 'crossorigin\"' not in include_moment

include_moment = _moment.include_moment(version='2.8.0')
_check_assertions()
include_moment = _moment.include_moment(version='2.3.1')
_check_assertions()
include_moment = _moment.include_moment(version='2.16.8')
_check_assertions()
include_moment = _moment.include_moment(version='2.30.1')
_check_assertions()

def test_moment_with_default_version(self):
include_moment = _moment.include_moment()

assert include_moment.startswith('<script src="//cdnjs.cloudflare.com'
'/ajax/libs/moment.js/2.18.1/moment-'
'with-locales.min.js" integrity='
'"sha384-iMhq1oHAQWG7+cVzHBvYynTbGZ'
'yO4DniLR7bhY1Q39AMn8ePTV9uByV/06g2xq'
'OS" crossorigin="anonymous">'
'</script>')

def test_moment_from_cdn_with_custom_sri_hash(self):
include_moment = _moment.include_moment(sri='sha384-12345678')

assert include_moment.startswith('<script src="//cdnjs.cloudflare.com'
'/ajax/libs/moment.js/2.18.1/moment-'
'with-locales.min.js" integrity='
'"sha384-12345678" crossorigin='
'"anonymous"></script>')

include_moment = _moment.include_moment(version='2.0.0',
sri='sha384-12345678')

assert include_moment.startswith('<script src="//cdnjs.cloudflare.com'
'/ajax/libs/moment.js/2.0.0/moment-'
'with-langs.min.js" integrity="sha384'
'-12345678" crossorigin="anonymous">'
'</script>')

def test_moment_local(self):
include_moment = _moment.include_moment(local_js=True)

assert 'src=\"' in include_moment
assert 'integrity=\"' not in include_moment
assert 'crossorigin\"' not in include_moment

def test_moment_local_with_sri(self):
include_moment = _moment.include_moment(local_js=True,
sri='sha384-87654321')

assert 'src=\"' in include_moment
assert 'integrity=\"sha384-87654321\"' in include_moment
assert 'crossorigin=\"anonymous\"' in include_moment

def test_disabling_moment_default(self):
include_moment = _moment.include_moment(sri=False)

assert 'src=\"' in include_moment
assert 'integrity=\"' not in include_moment
assert 'crossorigin' not in include_moment

def test_disabling_moment_custom(self):
include_moment = _moment.include_moment(local_js=True, sri=False)

assert 'src=\"' in include_moment
assert 'integrity=\"' not in include_moment
assert 'crossorigin' not in include_moment

def test_disabling_moment_custom_version(self):
include_moment = _moment.include_moment(version='2.17.9', sri=False)

assert 'src=\"' in include_moment
assert 'integrity=\"' not in include_moment
assert 'crossorigin' not in include_moment

def test_default_hash_values(self):
def _sri_hash(data):
h = hashlib.sha384(data).digest()
h_64 = base64.b64encode(h).decode()
return 'sha384-{}'.format(h_64)

def _get_data(url):
response = request.urlopen(url)
data = response.read()
return data

pattern = 'integrity=\"(.+?)\"'
include_jquery = _moment.include_jquery()
include_moment = _moment.include_moment()

# JQUERY
h_64 = _sri_hash(
_get_data('https://code.jquery.com/jquery-2.1.0.min.js'))

m = re.search(pattern, include_jquery)

assert m.group(1) == h_64

# MOMENT
h_64 = _sri_hash(
_get_data('https://cdnjs.cloudflare.com/ajax/libs/moment.js'
'/2.18.1/moment-with-locales.min.js'))

m = re.search(pattern, include_moment)
assert m.group(1) == h_64

0 comments on commit d87eaf8

Please sign in to comment.