Product Design

Feb. 4, 2019

/ code & tools

Content Security Policy in Flask and Django part 2

Wiktor Gonczaronek

The second part of the article about dealing with XSS (Cross Site Scripting) attack using Content Security Policy (CSP). This time I’ll show the implementation of CSP in Python frameworks - Django and Flask. Let’s practice!

CSP Implementation in Flask

So you’ve already got to know the Attack Scenario. Now we can check the CSP solution of the problem in Flask and Django. It seems that rescue is within a few keyboard clicks. The only changes I introduce in my project is adding an endpoint for receiving reports which will be printed out to console and adding a wrapper to secure my only view in the Flask application I’ve created.

The only two changes I had to make are here (one for import, one for decorating the view): 

+from flask import Flask, render_template, request, make_response
+
+from csp import csp

app = Flask(__name__)


@app.route('/', methods=['GET', 'POST'])
+@csp
def home():

 The code implementing changes is also rather uncomplicated.

SETTINGS_REPORT_CSP = {

   'default-src': [
       '\'self\'',
   ],
   'script-src': [
       '\'self\'',
       '\'sha256-pu1SeSVRQwuLmgKFdpgMzTi7ghm2F3Ldi/iw1Ff6Myc=\''
   ]

}
 

# Same rules for CSP, except for this will only report violations.
SETTINGS_REPORT_CSP = {}
REPORT_URI = '/report-csp-violations'

 
def make_csp_header(settings, report_uri=None):
   header = ''
   for directive, policies in settings.items():
       header += f'{directive} '
       header += ' '.join(
           (policy for policy in policies)
       )
       header += ';'
   if report_uri:
       header += f' report-uri {report_uri}'
   return header


def csp(func):
   @wraps(func)
   def _csp(*args, **kwargs):
       response = make_response(func(*args, **kwargs))
       if SETTINGS_REPORT_CSP:
           response.headers[
               'Content-Security-Policy-Report-Only'
           ] = make_csp_header(SETTINGS_REPORT_CSP, REPORT_URI)
       if SETTINGS_CSP:
           response.headers[
               'Content-Security-Policy'
           ] = make_csp_header(SETTINGS_CSP, REPORT_URI)
       return response
   return _csp

Let’s stop here and talk a little bit about some of the details. Settings are kept as a dictionary enumerating directives and sources. In our case the default-src will apply to CSS stylesheets, allowing those of them located under http://localhost:5000/* to load. Same applies to JavaScript files with one exception. Here we do allow an inline script whose contents we’ve calculated using sha-256 algorithm and then by encoding the result with base64. This means strictly contents of this script:

<script>console.log('This code was executed in an inline script')</script>

If we change a typo inside of it, we have to recalculate its hash and encode it. If we, on the other hand, want to change tag’s class or id, setting it to <script id=”some-script”>, no further changes need to be introduced, since only contents are taken into consideration.

Only report policy has been set, so we expect the malicious code to execute, but the browser should notify us about this fact and send a report to '/report-csp-violations'.

Testing

Let’s try our code again. How will it respond to class1 onclick=alert(1)?

As you can see, Chrome is aware of the problem and has sent a proper report to our endpoint which simply logs it to the console: 

@app.route('/report-csp-violations', methods=['POST'])
def report():
   pprint.pprint(json.loads(str(request.data, 'utf-8')))
   response = make_response()
   response.status_code = 200
   return response

Output:

127.0.0.1 - - [28/Jan/2019 13:06:40] "POST /report-csp-violations HTTP/1.1" 200 -
{'csp-report': {'blocked-uri': 'inline',
                'document-uri': 'http://localhost:5000/',
                'original-policy': "default-src 'self'; script-src 'self' "
                                   "'sha256-pu1SeSVRQwuLmgKFdpgMzTi7ghm2F3Ldi/iw1Ff6Myc='; "
                                   'report-uri '
                                   'http://localhost:5000/report-csp-violations',
                'referrer': 'http://localhost:5000/',
                'source-file': 'http://localhost:5000/',
                'violated-directive': 'script-src'}}

Malforming links in the dropdown list will also be stopped (when we change the header from report only to actual prevention):

[Report Only] Refused to load the script 'https://pastebin.com/raw/R570EE00' because it violates the following Content Security Policy directive: "script-src 'self' 'sha256-pu1SeSVRQwuLmgKFdpgMzTi7ghm2F3Ldi/iw1Ff6Myc='". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

What is also important for us is the fact that in the parts of the website we wanted to work, no problems occur. We can still choose one of the themes:

And customize background scripts behave:

The inline script we had earlier also is up and running:

At this point, we could change the policy to actually block the crackers going after our precious website. In the project, I have also prepared an example using nonce which creates a disposable token for each resource. This solution requires more work but is still useful. The security decorator will look like this:

SETTINGS_REPORT_CSP = {
   'default-src': [
       '\'self\'',
   ],
   'script-src': [
       '\'self\'',
       '\'nonce-{}\''
   ]
}
REPORT_URI = '/report-csp-violations'


def get_nonce():
   return secrets.token_hex()


def make_csp_header(settings, nonce, report_uri=None):
   header = ''
   for directive, policies in settings.items():
       header += f'{directive} '
       header += ' '.join(
           (
               policy if 'nonce' not in policy
               else policy.format(nonce)
               for policy in policies
           )
       )
       header += ';'
   if report_uri:
       header += f' report-uri {report_uri}'
   return header


def csp(func):
   @wraps(func)
   def _csp(*args, **kwargs):
       nonce = get_nonce()
       response = make_response(func(*args, **kwargs, inline_script_nonce=nonce))
       if SETTINGS_REPORT_CSP:
           response.headers[
               'Content-Security-Policy-Report-Only'
           ] = make_csp_header(SETTINGS_REPORT_CSP, nonce, REPORT_URI)
       if SETTINGS_CSP:
           response.headers[
               'Content-Security-Policy'
           ] = make_csp_header(SETTINGS_CSP, nonce)
       return response
   return _csp

With newly defined get_nonce() function that uses Python’s cryptographic function to generate a token, function responsible for creating header now has to discover the where to put it (it can’t be set in settings since nonce is generated per request), and we pass it to our view which uses it while rendering the template:

@app.route('/', methods=['GET', 'POST'])
@csp
def home(inline_script_nonce):
   context = {
       'css_class': 'class1',
       'additional_script': None,
       'inline_script_nonce': inline_script_nonce
   }
   if request.method == 'POST':
       if request.form['css_class']:
           context['css_class'] = request.form['css_class']
       if request.form['additional_script']:
           context['additional_script'] = request.form['additional_script']
   return render_template('home.html', **context) 

Little changes to the script and here we go:

<script nonce="{{ inline_script_nonce }}">console.log('This code was executed in an inline script')</script> 

Final setup

Once we’ve ensured that we have defined our policies correctly we can move on to changing it from report-only mode to actually blocking malicious input. We introduce following changes in our source code (the light-blue color on the left indicates what has been changed):

SETTINGS_CSP = {
   'default-src': [
       '\'self\'',
   ],
   'script-src': [
       '\'self\'',
       '\'sha256-pu1SeSVRQwuLmgKFdpgMzTi7ghm2F3Ldi/iw1Ff6Myc=\''
   ]
}


# Same rules for CSP, except for this will only report violations.
SETTINGS_REPORT_CSP = {}
REPORT_URI = '/report-csp-violations'


def make_csp_header(settings, report_uri=None):
   header = ''
   for directive, policies in settings.items():
       header += f'{directive} '
       header += ' '.join(
           (policy for policy in policies)
       )
       header += ';'
   if report_uri:
       header += f' report-uri {report_uri}'
   return header


def csp(func):
   @wraps(func)
   def _csp(*args, **kwargs):
       response = make_response(func(*args, **kwargs))
       if SETTINGS_REPORT_CSP:
           response.headers[
               'Content-Security-Policy-Report-Only'
           ] = make_csp_header(SETTINGS_REPORT_CSP, REPORT_URI)
       if SETTINGS_CSP:
           response.headers[
               'Content-Security-Policy'
           ] = make_csp_header(SETTINGS_CSP, REPORT_URI)
       return response
   return _csp

 And now let’s check whether any of the problems persist. The malicious input in CSS class selector has been detected, stopped and reported.

Content Security Policy: The page’s settings blocked the loading of a resource at inline (“script-src”).

No alert box:

And a part of the report logged by server:

127.0.0.1 - - [28/Jan/2019 13:06:40] "POST /report-csp-violations HTTP/1.1" 200 -
{'csp-report': {'blocked-uri': 'inline',
                'document-uri': 'http://localhost:5000/',
                'original-policy': "default-src 'self'; script-src 'self' "
                                   "'sha256-pu1SeSVRQwuLmgKFdpgMzTi7ghm2F3Ldi/iw1Ff6Myc='; "
                                   'report-uri '
                                   'http://localhost:5000/report-csp-violations',
                'referrer': 'http://localhost:5000/',
                'source-file': 'http://localhost:5000/',
                'violated-directive': 'script-src'}}

Trying to load script from untrusted source has also failed:

(index):1 Refused to load the script 'https://pastebin.com/raw/R570EE00' because it violates the following Content Security Policy directive: "script-src 'self' 'sha256-pu1SeSVRQwuLmgKFdpgMzTi7ghm2F3Ldi/iw1Ff6Myc='". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

And our backend has been informed about that fact:

127.0.0.1 - - [28/Jan/2019 13:15:59] "POST /report-csp-violations HTTP/1.1" 200 -
{'csp-report': {'blocked-uri': 'https://pastebin.com/raw/R570EE00',
                'disposition': 'enforce',
                'document-uri': 'http://localhost:5000/',
                'effective-directive': 'script-src-elem',
                'original-policy': "default-src 'self';script-src 'self' "
                                   "'sha256-pu1SeSVRQwuLmgKFdpgMzTi7ghm2F3Ldi/iw1Ff6Myc='; "
                                   'report-uri /report-csp-violations',
                'referrer': 'http://localhost:5000/',
                'script-sample': '',
                'status-code': 200,
                'violated-directive': 'script-src-elem'}}

And we were also able to maintain site’s functionality:

Content Security Policy in Django

In this example I have implemented the policy from scratch, manually adding proper headers, in order to show the implementation in details. Django unfortunately, does not provide any built-in mechanisms that we could make use of, but fortunately, Mozilla Foundation has created a library that could be used immediately. Just import this library, use settings listed in the docs and you are ready to go. All the work we have done could be limited to these lines of code:

INSTALLED_APPS = [
   # ...
   'csp',
    # ...
]
MIDDLEWARE = [
   # ...
   'csp.middleware.CSPMiddleware',
]


# CSP
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'sha256-pu1SeSVRQwuLmgKFdpgMzTi7ghm2F3Ldi/iw1Ff6Myc='")
CSP_REPORT_URI = '/report-csp-violation/'

And an endpoint to log violations:

def report_csp_violation(request):
   pprint.pprint(
       json.loads(request.body.decode('utf-8'))
   )
   return HttpResponse(status=200)

If we want to change the policy for a specific view, appropriate decorators like @csp_exempt or @csp_update are available.

CSP - report-only mode helps 

The implementation of CSP may be pretty straightforward. Thanks to the possibility of the report-only mode, it can be set up immediately without risking any downtime to our services due to misconfigurations with an option of adjusting it while it’s being used.

Please, keep in mind, however, that:

  • Using CPS does not solve your bugs. They still exist, although harder to exploit.
  • This article is not a complete guide or technical reference. In order to ensure that you used CSP mechanism correctly, consult your solution with a professional.

In order to view the project please visit Merixstudio's Git Hub. All versions of what we’ve done here are tagged properly if you would like to review differences. See also django-trench - our own open-source library providing multi-factor authentication for Django.  

If you’re interested in rising your programming skills in the security area, why don’t do it our backend team? Here’re our job offers for backend developers! 

 

We use cookies on this site to improve performance. By browsing this site you are agreeing to this. For more information see our Privacy policy.