Skip to content

Commit

Permalink
Add report-only CSP and force-file-save options
Browse files Browse the repository at this point in the history
* Document X-XSS-Protection and X-Content-Type-Options headers
* Add X-Download-Options header
* Add content_security_policy_report_only option to enable report-only CSP
* Add content_security_policy_report_uri parameter
* Add parameter for setting X-Download-Options header
  • Loading branch information
jezdez authored and Jon Wayne Parrott committed Sep 2, 2016
1 parent 6cfafb8 commit 1dac22d
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 22 deletions.
42 changes: 31 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ The default configuration:

- Forces all connects to ``https``, unless running with debug enabled.
- Enables `HTTP Strict Transport
Security <https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security>`__.
Security <https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security>`_.
- Enables HSTS preloading. If you register your application with
`Google's HSTS preload list <https://hstspreload.appspot.com/>`__,
`Google's HSTS preload list <https://hstspreload.appspot.com/>`_,
Firefox and Chrome will never load your site over a non-secure
connection.
- Sets Flask's session cookie to ``secure``, so it will never be set if
Expand All @@ -22,25 +22,34 @@ The default configuration:
from being able to access its content. CSRF via Ajax uses a separate
cookie and should be unaffected.
- Sets
`X-Frame-Options <https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options>`__
`X-Frame-Options <https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options>`_
to ``SAMEORIGIN`` to avoid
`clickjacking <https://en.wikipedia.org/wiki/Clickjacking>`__.
`clickjacking <https://en.wikipedia.org/wiki/Clickjacking>`_.
- Sets `X-XSS-Protection
<http://msdn.microsoft.com/en-us/library/dd565647(v=vs.85).aspx>`_ to enable
a cross site scripting filter for IE/Chrome.
- Sets `X-Content-Type-Options
<https://msdn.microsoft.com/library/gg622941(v=vs.85).aspx>`_ to prevents
content type sniffing for IE >= 9.
- Sets `X-Download-Options
<https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx>`_ to prevent
file downloads opening for IE >= 8.
- Sets a strict `Content Security
Policy <https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Introducing_Content_Security_Policy>`__
Policy <https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Introducing_Content_Security_Policy>`_
of ``default-src: 'self'``. This is intended to almost completely
prevent Cross Site Scripting (XSS) attacks. This is probably the only
setting that you should reasonably change. See the `section
below <#content-security-policy>`__ on configuring this.
below <#content-security-policy>`_ on configuring this.

In addition to Talisman, you **should always use a cross-site request
forgery (CSRF) library**. It's highly recommended to use
`Flask-SeaSurf <https://flask-seasurf.readthedocs.org/en/latest/>`__,
`Flask-SeaSurf <https://flask-seasurf.readthedocs.org/en/latest/>`_,
which is based on Django's excellent library.

Installation & Basic Usage
--------------------------

Install via `pip <https://pypi.python.org/pypi/pip>`__:
Install via `pip <https://pypi.python.org/pypi/pip>`_:

::

Expand All @@ -56,7 +65,7 @@ Install via `pip <https://pypi.python.org/pypi/pip>`__:
Talisman(app)
There is also a full `Example App <https://github.com/GoogleCloudPlatform/flask-talisman/blob/master/example_app>`__.
There is also a full `Example App <https://github.com/GoogleCloudPlatform/flask-talisman/blob/master/example_app>`_.

Options
-------
Expand All @@ -76,11 +85,22 @@ Options
- ``strict_transport_security_include_subdomains``, default ``True``,
whether subdomains should also use HSTS.
- ``content_security_policy``, default ``default-src: 'self'``, see the
`section below <#content-security-policy>`__.
`section below <#content-security-policy>`_.
- ``content_security_policy_report_only``, default ``False``, whether to set
the CSP header as "report-only" (as `Content-Security-Policy-Report-Only`)
to ease deployment by disabling the policy enforcement by the browser,
requires passing a value with the ``content_security_policy_report_uri``
parameter
- ``content_security_policy_report_uri``, default ``None``, a string
indicating the report URI used for `CSP violation reports
<https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Using_CSP_violation_reports>`_
- ``session_cookie_secure``, default ``True``, set the session cookie
to ``secure``, preventing it from being sent over plain ``http``.
- ``session_cookie_http_only``, default ``True``, set the session
cookie to ``httponly``, preventing it from being read by JavaScript.
- ``force_file_save``, default ``False``, whether to set the
``X-Download-Options`` header to ``noopen`` to prevent IE >= 8 to from
opening file downloads directly and only save them instead

Per-view options
~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -119,7 +139,7 @@ libraries, fonts, and embeding media from YouTube and Maps.

You can and should create your own policy to suit your site's needs.
Here's a few examples adapted from
`MDN <https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Using_Content_Security_Policy>`__:
`MDN <https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Using_Content_Security_Policy>`_:

Example 1
~~~~~~~~~
Expand Down
42 changes: 38 additions & 4 deletions talisman/talisman.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ def init_app(
app,
force_https=True,
force_https_permanent=False,
force_file_save=False,
frame_options=SAMEORIGIN,
frame_options_allow_from=None,
strict_transport_security=True,
strict_transport_security_max_age=ONE_YEAR_IN_SECS,
strict_transport_security_include_subdomains=True,
content_security_policy=DEFAULT_CSP_POLICY,
content_security_policy_report_uri=None,
content_security_policy_report_only=False,
session_cookie_secure=True,
session_cookie_http_only=True):
"""
Expand All @@ -86,10 +89,18 @@ def init_app(
all subdomains when setting HSTS.
content_security_policy: A string or dictionary describing the
content security policy for the response.
content_security_policy_report_uri: A string indicating the report
URI used for CSP violation reports
content_security_policy_report_only: Whether to set the CSP header
as "report-only", which disables the enforcement by the browser
and requires a "report-uri" parameter with a backend to receive
the POST data
session_cookie_secure: Forces the session cookie to only be sent
over https. Disabled in debug mode.
session_cookie_http_only: Prevents JavaScript from reading the
session cookie.
force_file_save: Prevents the user from opening a file download
directly on >= IE 8
See README.rst for a detailed description of each option.
"""
Expand All @@ -101,18 +112,30 @@ def init_app(
self.frame_options_allow_from = frame_options_allow_from

self.strict_transport_security = strict_transport_security
self.strict_transport_security_max_age =\
self.strict_transport_security_max_age = \
strict_transport_security_max_age
self.strict_transport_security_include_subdomains =\
self.strict_transport_security_include_subdomains = \
strict_transport_security_include_subdomains

self.content_security_policy = content_security_policy.copy()
self.content_security_policy_report_uri = \
content_security_policy_report_uri
self.content_security_policy_report_only = \
content_security_policy_report_only
if self.content_security_policy_report_only and \
self.content_security_policy_report_uri is None:
raise ValueError(
'Setting content_security_policy_report_only to True also '
'requires a URI to be specified in '
'content_security_policy_report_uri')

self.session_cookie_secure = session_cookie_secure

if session_cookie_http_only:
app.config['SESSION_COOKIE_HTTPONLY'] = True

self.force_file_save = force_file_save

self.app = app
self.local_options = Local()

Expand Down Expand Up @@ -179,6 +202,9 @@ def _set_content_security_policy_headers(self, headers):
headers['X-XSS-Protection'] = '1; mode=block'
headers['X-Content-Type-Options'] = 'nosniff'

if self.force_file_save:
headers['X-Download-Options'] = 'noopen'

if not self.local_options.content_security_policy:
return

Expand All @@ -195,9 +221,17 @@ def _set_content_security_policy_headers(self, headers):

policy = '; '.join(policies)

headers['Content-Security-Policy'] = policy
if self.content_security_policy_report_uri and \
'report-uri' not in policy:
policy += '; report-uri ' + self.content_security_policy_report_uri

csp_header = 'Content-Security-Policy'
if self.content_security_policy_report_only:
csp_header += '-Report-Only'

headers[csp_header] = policy
# IE 10-11, Older Firefox.
headers['X-Content-Security-Policy'] = policy
headers['X-' + csp_header] = policy

def _set_hsts_headers(self, headers):
if not self.strict_transport_security or not flask.request.is_secure:
Expand Down
52 changes: 45 additions & 7 deletions talisman/talisman_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,19 @@ def testHstsOptions(self):

# No HSTS headers for non-ssl requests
response = self.client.get('/')
self.assertTrue('Strict-Transport-Security' not in response.headers)
self.assertFalse('Strict-Transport-Security' in response.headers)

# Secure request with HSTS off
self.talisman.strict_transport_security = False
response = self.client.get('/', environ_overrides=HTTPS_ENVIRON)
self.assertTrue('Strict-Transport-Security' not in response.headers)
self.assertFalse('Strict-Transport-Security' in response.headers)

# No subdomains
self.talisman.strict_transport_security = True
self.talisman.strict_transport_security_include_subdomains = False
response = self.client.get('/', environ_overrides=HTTPS_ENVIRON)
self.assertTrue(
'includeSubDomains' not in
self.assertFalse(
'includeSubDomains' in
response.headers['Strict-Transport-Security'])

def testFrameOptions(self):
Expand Down Expand Up @@ -117,7 +117,7 @@ def testContentSecurityPolicyOptions(self):
self.assertTrue('default-src \'self\'' in csp)
self.assertTrue('image-src \'self\' example.com' in csp)

# sting policy
# string policy
self.talisman.content_security_policy = 'default-src example.com'
response = self.client.get('/', environ_overrides=HTTPS_ENVIRON)
self.assertEqual(response.headers['Content-Security-Policy'],
Expand All @@ -126,7 +126,39 @@ def testContentSecurityPolicyOptions(self):
# no policy
self.talisman.content_security_policy = False
response = self.client.get('/', environ_overrides=HTTPS_ENVIRON)
self.assertTrue('Content-Security-Policy' not in response.headers)
self.assertFalse('Content-Security-Policy' in response.headers)

def testContentSecurityPolicyOptionsReport(self):
# report-only policy
self.talisman.content_security_policy_report_only = True
self.talisman.content_security_policy_report_uri = \
'https://example.com'
response = self.client.get('/', environ_overrides=HTTPS_ENVIRON)
self.assertTrue(
'Content-Security-Policy-Report-Only' in response.headers)
self.assertTrue(
'X-Content-Security-Policy-Report-Only' in response.headers)
self.assertTrue(
'report-uri'
in response.headers['Content-Security-Policy-Report-Only'])
self.assertFalse('Content-Security-Policy' in response.headers)
self.assertFalse('X-Content-Security-Policy' in response.headers)

override_report_uri = 'https://report-uri.io/'
self.talisman.content_security_policy = {
'report-uri': override_report_uri,
}
response = self.client.get('/', environ_overrides=HTTPS_ENVIRON)
self.assertTrue(
'Content-Security-Policy-Report-Only' in response.headers)
self.assertTrue(
override_report_uri
in response.headers['Content-Security-Policy-Report-Only']
)

# exception on missing report-uri when report-only
self.assertRaises(ValueError, Talisman, self.app,
content_security_policy_report_only=True)

def testDecorator(self):

Expand All @@ -136,5 +168,11 @@ def nocsp():
return 'Hello, world'

response = self.client.get('/nocsp', environ_overrides=HTTPS_ENVIRON)
self.assertTrue('Content-Security-Policy' not in response.headers)
self.assertFalse('Content-Security-Policy' in response.headers)
self.assertEqual(response.headers['X-Frame-Options'], 'SAMEORIGIN')

def testForceFileSave(self):
self.talisman.force_file_save = True
response = self.client.get('/', environ_overrides=HTTPS_ENVIRON)
self.assertTrue('X-Download-Options' in response.headers)
self.assertEqual(response.headers['X-Download-Options'], 'noopen')

0 comments on commit 1dac22d

Please sign in to comment.