Skip to content

Commit

Permalink
Add example for CSP nonce & Jinja template integration (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
moser authored and Jon Wayne Parrott committed Mar 8, 2018
1 parent 638dbe2 commit c5edeaf
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ docs/_build/

# PyBuilder
target/

# Editor files
*.swp
*.swo
29 changes: 29 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,35 @@ example CSP, this site uses the setting specified by the ``default-src``
directive, which means that scripts can be loaded only from the
originating server.

Example 6
~~~~~~~~~

A web site administrator wants to allow embedded scripts (which might
be generated dynamicially).

.. code:: python
csp = {
'default-src': '\'self\'',
'script-src': '\'self\'',
}
talisman = Talisman(
app,
content_security_policy=csp,
content_security_policy_nonce_in=['script-src']
)
The nonce needs to be added to the script tag in the template:

.. code:: html

<script nonce="{{ csp_nonce() }}">
//...
</script>

Note that the CSP directive (`script-src` in the example) to which the `nonce-...`
source should be added needs to be defined explicitly.

Disclaimer
----------

Expand Down
29 changes: 28 additions & 1 deletion example_app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,24 @@
app = Flask(__name__)
app.secret_key = '123abc'
csrf = SeaSurf(app)
talisman = Talisman(app)

SELF = "'self'"
talisman = Talisman(
app,
content_security_policy={
'default-src': SELF,
'img-src': '*',
'script-src': [
SELF,
'some.cdn.com',
],
'style-src': [
SELF,
'another.cdn.com',
],
},
content_security_policy_nonce_in=['script-src'],
)


@app.route('/', methods=['GET', 'POST'])
Expand All @@ -29,5 +46,15 @@ def index():
return render_template('index.html', message=message)


# Example of a route-specific talisman configuration
@app.route('/embeddable')
@talisman(
frame_options='ALLOW-FROM',
frame_options_allow_from='https://example.com/',
)
def embeddable():
return "<html>I can be embedded.</html>"


if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)
10 changes: 10 additions & 0 deletions example_app/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,15 @@ <h3>Your message:</h3>
<button type="submit">Submit</button>
</form>

<script>
// This script is forbidden
console.log("Oh no, this should not have run!!")
</script>

<script nonce="{{ csp_nonce() }}">
// This one isn't
console.log("Yay, nonce allowed to run this.")
</script>

</body>
</html>
5 changes: 5 additions & 0 deletions flask_talisman/talisman.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ def init_app(
content_security_policy_nonce_in or []
)

app.jinja_env.globals['csp_nonce'] = self._get_nonce

self.referrer_policy = referrer_policy

self.session_cookie_secure = session_cookie_secure
Expand Down Expand Up @@ -232,6 +234,9 @@ def _make_nonce(self):
not getattr(flask.request, 'csp_nonce', None)):
flask.request.csp_nonce = get_random_string(NONCE_LENGTH)

def _get_nonce(self):
return getattr(flask.request, 'csp_nonce', '')

def _set_frame_options_headers(self, headers):
headers['X-Frame-Options'] = self.local_options.frame_options

Expand Down
20 changes: 18 additions & 2 deletions flask_talisman/talisman_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,25 @@
HTTPS_ENVIRON = {'wsgi.url_scheme': 'https'}


def hello_world():
return 'Hello, world'


def with_nonce():
return flask.render_template_string(
'<script nonce="{{csp_nonce()}}"></script>'
)


class TestTalismanExtension(unittest.TestCase):

def setUp(self):
self.app = flask.Flask(__name__)
self.talisman = Talisman(self.app)
self.client = self.app.test_client()

self.app.route('/')(lambda: 'Hello, world')
self.app.route('/')(hello_world)
self.app.route('/with_nonce')(with_nonce)

def testDefaults(self):
# HTTPS request.
Expand Down Expand Up @@ -182,14 +193,19 @@ def testContentSecurityPolicyNonce(self):
self.talisman.content_security_policy_nonce_in = ['script-src']

with self.app.test_client() as client:
response = client.get('/', environ_overrides=HTTPS_ENVIRON)
response = client.get('/with_nonce',
environ_overrides=HTTPS_ENVIRON)

csp = response.headers['Content-Security-Policy']

self.assertIn(
"script-src 'self' 'nonce-{}'".format(flask.request.csp_nonce),
csp
)
self.assertIn(
flask.request.csp_nonce,
response.data.decode("utf-8")
)

def testDecorator(self):
@self.app.route('/nocsp')
Expand Down

0 comments on commit c5edeaf

Please sign in to comment.