Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #1329, #1328. Add a CSP reporting endpoint and move security headers into app. #1367

Merged
merged 5 commits into from
Mar 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
if not LOCALHOST:
SESSION_COOKIE_SECURE = True

# By default, we want to log CSP violations. See /csp-report in views.py.
CSP_LOG = True

# Logging Capabilities
# To benefit from the logging, you may want to add:
# app.logger.info(Thing_To_Log)
Expand All @@ -38,6 +41,7 @@

LOG_FILE = '/tmp/webcompat.log'
LOG_FMT = '%(asctime)s tracking %(message)s'
CSP_REPORTS_LOG = '/tmp/webcompat-csp-reports.log'

# Status categories used in the project
# 'new', 'needsdiagnosis', 'needscontact', 'contactready' , 'sitewait', 'close'
Expand Down
13 changes: 13 additions & 0 deletions tests/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ def test_labeler_webhook(self):
# A random post should 401, only requests from GitHub will 200
self.assertEqual(rv.status_code, 401)

def test_csp_report_uri(self):

This comment was marked as abuse.

This comment was marked as abuse.

This comment was marked as abuse.

'''Test POST to /csp-report w/ correct content-type returns 204.'''
headers = {'Content-Type': 'application/csp-report'}
rv = self.app.post('/csp-report', headers=headers)
self.assertEqual(rv.status_code, 204)

def test_csp_report_uri_bad_content_type(self):

This comment was marked as abuse.

This comment was marked as abuse.

'''Test POST w/ wrong content-type to /csp-report returns 400.'''
headers = {'Content-Type': 'application/json'}
rv = self.app.post('/csp-report', headers=headers)
self.assertNotEqual(rv.status_code, 204)
self.assertEqual(rv.status_code, 400)

def test_tools_cssfixme(self):
'''Test that the /tools/cssfixme route gets 200.'''
rv = self.app.get('/tools/cssfixme')
Expand Down
31 changes: 31 additions & 0 deletions webcompat/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,3 +487,34 @@ def policy(*args, **kwargs):
return response
return update_wrapper(policy, view)
return set_policy


def add_sec_headers(response):
'''Add security-related headers to the response.

This should be used in @app.after_request to ensure the headers are
added to all responses.'''
if not app.config['LOCALHOST']:
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' # nopep8
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['X-Frame-Options'] = 'DENY'


def add_csp(response):
'''Add a Content-Security-Policy header to response.

This should be used in @app.after_request to ensure the header is
added to all responses.'''
# short term, we send Content-Security-Policy-Report-Only
# see https://github.com/webcompat/webcompat.com/issues/763 for
# sending Content-Security-Policy
response.headers['Content-Security-Policy-Report-Only'] = (
"default-src 'none'; " +
"connect-src 'self'; " +
"font-src 'self'; " +
"img-src 'self'; " +
"script-src 'self' https://www.google-analytics.com; " +
"style-src 'self'; " +
"report-uri /csp-report"
)
23 changes: 23 additions & 0 deletions webcompat/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

from form import AUTH_REPORT
from form import PROXY_REPORT
from helpers import add_csp
from helpers import add_sec_headers
from helpers import cache_policy
from helpers import get_browser_name
from helpers import get_form
Expand Down Expand Up @@ -52,6 +54,8 @@ def before_request():
@app.after_request
def after_request(response):
session_db.remove()
add_sec_headers(response)
add_csp(response)
return response


Expand Down Expand Up @@ -315,3 +319,22 @@ def contributors():
def cssfixme():
'''Route for CSS Fix me tool'''
return render_template('cssfixme.html')


@app.route('/csp-report', methods=['POST'])
def log_csp_report():
'''Route to record CSP header violations.

This route can be enabled/disabled by setting CSP_LOG to True/False
in config/__init__.py. It's enabled by default.
'''
expected_mime = 'application/csp-report'

if app.config['CSP_LOG']:
if expected_mime not in request.headers.get('content-type', ''):
return ('Wrong Content-Type.', 400)
with open(app.config['CSP_REPORTS_LOG'], 'a') as r:
r.write(request.data + '\n')
return ('', 204)
else:
return ('Forbidden.', 403)