#279 Add a system for configuring the Content Security Policy and a default strict CSP
Opened 2 years ago by puiterwijk. Modified 2 years ago
puiterwijk/ipsilon csp  into  master

file modified
+4 -1

@@ -4,7 +4,7 @@ 

  import logging

  from ipsilon.util.page import Page

  from ipsilon.util.page import admin_protect

- from ipsilon.util.endpoint import allow_iframe

+ from ipsilon.util.endpoint import allow_iframe, override_csp

  from ipsilon.util import config as pconfig

  

  

@@ -301,6 +301,7 @@ 

          self.url = '%s/%s' % (self.basepath, mount)

          self.menu = [self]

  

+     @override_csp({'object-src': "'self'", 'frame-src': "'self'"})

      def root(self, *args, **kwargs):

          return self._template('admin/index.html',

                                title='Configuration',

@@ -324,6 +325,8 @@ 

          return urls

  

      @admin_protect

+     @override_csp({'frame-ancestors': '*',

+                    'style-src': "'unsafe-inline'"})

      @allow_iframe

      def scheme(self):

          cherrypy.response.headers.update({'Content-Type': 'image/svg+xml'})

@@ -4,6 +4,7 @@ 

  from ipsilon.providers.common import InvalidRequest, UnauthorizedRequest

  from ipsilon.providers.openid.meta import XRDSHandler, UserXRDSHandler

  from ipsilon.providers.openid.meta import IDHandler

+ from ipsilon.util.endpoint import override_csp

  from ipsilon.util.policy import Policy

  from ipsilon.util.trans import Transaction

  from ipsilon.util.user import UserSession

@@ -221,6 +222,7 @@ 

                  response.addExtension(resp)

          return response

  

+     @override_csp({'form-action': '*'})

      def _respond(self, response):

          try:

              self.debug('Response: %s' % response)

@@ -9,6 +9,7 @@ 

                                             UserInfo)

  from ipsilon.providers.openidc.provider import (get_url_hostpart,

                                                  Registration)

+ from ipsilon.util.endpoint import override_csp

  from ipsilon.util.user import UserSession

  

  from jwcrypto.jwt import JWT

@@ -61,6 +62,7 @@ 

          self.debug('Filterd attributes: %s' % repr(attributes))

          return attributes

  

+     @override_csp({'form-action': '*'})

      def _respond(self, request, contents):

          url = request['redirect_uri']

          response_mode = request.get('response_mode', None)

@@ -7,6 +7,7 @@ 

  from ipsilon.providers.saml2.provider import NameIdNotAllowed

  from ipsilon.tools import saml2metadata as metadata

  from ipsilon.util.policy import Policy

+ from ipsilon.util.endpoint import override_csp

  from ipsilon.util.user import UserSession

  from ipsilon.util.trans import Transaction

  import cherrypy

@@ -373,6 +374,7 @@ 

          status.statusCode.statusCode.value = code

          login.response.status = status

  

+     @override_csp({'form-action': '*'})

      def reply(self, login):

          if login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_ART:

              # TODO

file modified
+54 -6

@@ -12,6 +12,39 @@ 

      from urllib.parse import urlparse

  

  

+ def get_CSP(override=None):

+     """This method determines the Content Security Policy for the request.

+ 

+     The CSP we generate comes from the configuration file, with entries from

+     override added.

+ 

+     override: dict with specific CSP components to be added.

+     """

+     components = {'frame-ancestors': "'none'"}

+     # First load the values from the configuration

+     for cfg in cherrypy.config:

+         if cfg.startswith('csp.'):

+             components[cfg[4:]] = cherrypy.config[cfg]

+     # Take overrides into account

+     if override is None:

+         override = {}

+     for comp in override:

+         if comp not in components:

+             components[comp] = override[comp]

+         else:

+             if "'none'" in override[comp]:

+                 components[comp] = "'none'"

+             elif "'none'" in components[comp]:

+                 components[comp] = override[comp]

+             else:

+                 components[comp] += ' ' + override[comp]

+     # And generate the header the way browsers want it

+     header = ''

+     for key in components:

+         header += '%s %s; ' % (key, components[key])

+     return header

+ 

+ 

  def allow_iframe(func):

      """

      Remove the X-Frame-Options and CSP frame-ancestors deny headers.

@@ -19,16 +52,31 @@ 

      @wraps(func)

      def wrapper(*args, **kwargs):

          result = func(*args, **kwargs)

-         for (header, value) in [

-                 ('X-Frame-Options', 'deny'),

-                 ('Content-Security-Policy', 'frame-ancestors \'none\'')]:

-             if cherrypy.response.headers.get(header, None) == value:

-                 cherrypy.response.headers.pop(header, None)

+         cherrypy.response.headers['Content-Security-Policy'] = get_CSP(

+             {'frame-ancestors': '*'})

+         if cherrypy.response.headers.get('X-Frame-Options', None) == 'deny':

+             cherrypy.response.headers.pop('X-Frame-Options', None)

          return result

  

      return wrapper

  

  

+ def override_csp(overrides):

+     """

+     Overrides the Content Security Policy with specific components.

+     """

+     def wrapper(func):

+         @wraps(func)

+         def inwrapper(*args, **kwargs):

+             result = func(*args, **kwargs)

+             cherrypy.response.headers['Content-Security-Policy'] = get_CSP(

+                 overrides)

+             return result

+ 

+         return inwrapper

+     return wrapper

+ 

+ 

  class Endpoint(Log):

      def __init__(self, site):

          self._site = site

@@ -37,7 +85,7 @@ 

          self.default_headers = {

              'Cache-Control': 'no-cache, no-store, must-revalidate, private',

              'Pragma': 'no-cache',

-             'Content-Security-Policy': 'frame-ancestors \'none\'',

+             'Content-Security-Policy': get_CSP(),

              'X-Frame-Options': 'deny',

          }

          self.auth_protect = False

@@ -1,56 +1,6 @@ 

  {% extends "master-admin.html" %}

  {% block scripts %}

-     <script>

-         $( document ).on("click", ".add-field",

-             function() {

-                 var buttonRow = $(this).parents(".add-row")

-                 var ourTable = $(this).parents(".extensible-table")

-                 var lastRow = $(ourTable).find(".list-field:last")

-                 var newRow = $(lastRow).clone()

-                 lastRow.show()

-                 var inputFields = lastRow.find("input")

-                 for (i = 0; i < inputFields.length; i++) {

-                     $(inputFields[i]).prop("disabled", false)

-                 }

-                 var lastIndex = parseInt(newRow.find("td:first").text())

-                 newRow.find("td:first").text(lastIndex + 1)

-                 var inputFields = newRow.find("input")

-                 for (i = 0; i < inputFields.length; i++) {

-                     var separator = (lastIndex-1).toString()+"-"

-                     var nArr = $(inputFields[i]).attr("name").split(separator)

-                     var newidx = lastIndex.toString()+"-"

-                     if (nArr.length === 2) {

-                         $(inputFields[i]).attr("name", nArr[0]+newidx+nArr[1])

-                     }

-                     $(inputFields[i]).attr("value", "")

-                 }

-                 $(newRow).appendTo(ourTable)

-                 $(buttonRow).appendTo(ourTable)

-             }

-         );

-         $(function() {

-             $("#uploadFile").on("change", function()

-             {

-                 var files = !!this.files ? this.files : [];

-                 if (!files.length || !window.FileReader) return; // no file selected, or no FileReader support

-  

-                 if (/^image/.test( files[0].type)){ // only image file

-                     var reader = new FileReader(); // instance of the FileReader

-                     reader.readAsDataURL(files[0]); // read the local file

-  

-                     reader.onloadend = function(e){ // set image data as background of div

-                         var contents = e.target.result;

-                         if (!contents) {

-                             window.alert('Image file is unreadable')

-                             document.getElementById('uploadFile').value = null;

-                         }

- 

-                         $("#imagePreview").css("background-image", "url("+this.result+")");

-                     }

-                 }

-             });

-         });

-     </script>

+     <script src="{{ basepath }}/ui/js/pages/admin_option_config.js"></script>

  {% endblock %}

  {% block main %}

  

@@ -95,12 +45,11 @@ 

                  >

                  {%- endif -%}

                  <p></p>

-                 <input type="file" name="{{ v.name }}"

+                 <input class="hide" type="file" name="{{ v.name }}"

                       title="{{ v.name }}"

                       accept=".png,.jpg"

-                      id="uploadFile"

-                      style="display: none;" />

-                 <input type="button" value="Select Image..." onclick="document.getElementById('uploadFile').click();" />

+                      id="uploadFile" />

+                 <input class="image-select" type="button" value="Select Image..." />

                  <p></p>

                  <div id="imagePreview"></div>

                {% elif v.__class__.__name__ == 'List' -%}

@@ -181,7 +130,7 @@ 

                  {% endfor -%}

                  <!-- Template for new row -->

                  {%- set basename = "%s %d-"|format(v.name, value|length) -%}

-                 <tr class="list-field" style="display:none">

+                 <tr class="list-field hide">

                      <td>{{value|length + 1}}</td>

                      <td>

                        <input type="text" name="{{basename}}name" value=""

@@ -231,7 +180,7 @@ 

                  {% endfor -%}

                  <!-- Template for new row -->

                  {%- set basename = "%s %d-"|format(v.name, value|length) -%}

-                 <tr class="list-field" style="display:none">

+                 <tr class="list-field hide">

                      <td>{{value|length + 1}}</td>

                      <td>

                        <input type="text" name="{{basename}}from" value=""

@@ -1,4 +1,7 @@ 

  {% extends "master-admin.html" %}

+ {% block scripts %}

+     <script src="{{ basepath }}/ui/js/pages/admin_providers_openidc.js"></script>

+ {% endblock %}

  {% block main %}

  <h2>Clients</h2>

  

@@ -15,8 +18,7 @@ 

          </div>

          <div class="col-md-3 col-sm-3 col-xs-6">

               {{ clients[cid]['client_name'] }}

-             <a class="btn btn-default" href="{{ baseurl }}/client/{{ cid }}/delete"

-                 onclick="return confirm('Do you really want to remove this client?');">Delete</a>

+             <a class="btn btn-default confirm-delete" href="{{ baseurl }}/client/{{ cid }}/delete">Delete</a>

          </div>

      </div>

  {% endfor %}

@@ -70,38 +70,5 @@ 

          </form>

      </div>

  

- <script>

-     function verifyFile(filename, objid, failtext) {

-         var reader = new FileReader();

-         reader.readAsDataURL(filename); // read the local file

- 

-         reader.onloadend = function(e){

-             var contents = e.target.result;

-             if (!contents) {

-                 window.alert(failtext)

-                 document.getElementById(objid).value = null;

-             }

-         }

-     }

- 

-     $(function() {

-         $("#file").on("change", function()

-         {

-             var files = !!this.files ? this.files : [];

-             if (!files.length || !window.FileReader) return; // no file selected, or no FileReader support

- 

-             verifyFile(files[0], 'file', 'Metadata file is unreadable');

-         });

-     });

- 

-     $(function() {

-         $("#image").on("change", function()

-         {

-             var files = !!this.files ? this.files : [];

-             if (!files.length || !window.FileReader) return; // no file selected, or no FileReader support

- 

-             verifyFile(files[0], 'image', 'Image file is unreadable');

-         });

-     });

- </script>

+ <script src="{{ basepath }}/ui/js/pages/admin_providers_saml2_sp_new.js"></script>

  {% endblock %}

file modified
+1 -27

@@ -127,33 +127,7 @@ 

      <script src="{{ basepath }}/ui/js/patternfly.js"></script>

      <script src="{{ basepath }}/ui/js/divfilter.js"></script>

  

-     <script>

-       (function($) {

-         $(document).ready(function() {

-           // Hide the clear button if the search input is empty

-           $(".search-pf .has-clear .clear").each(function() {

-             if (!$(this).prev('.form-control').val()) {

-               $(this).hide();

-             }

-           });

-           // Show the clear button upon entering text in the search input

-           $(".search-pf .has-clear .form-control").keyup(function () {

-             var t = $(this);

-             t.next('button').toggle(Boolean(t.val()));

-           });

-           // Upon clicking the clear button, empty the entered text and hide the clear button

-           $(".search-pf .has-clear .clear").click(function () {

-             $(this).prev('.form-control').val('').focus();

-             $(this).hide();

-           });

-         });    

-         

-         $(function () {

-       $('[data-toggle="tooltip"]').tooltip()

-     })

-         

-       })(jQuery);

-     </script>      

+     <script src="{{ basepath }}/ui/js/pages/index.js"></script>

  

    </body>

  </html>

@@ -21,3 +21,11 @@ 

  tools.sessions.timeout = ${session_timeout}

  tools.sessions.httponly = ${secure}

  tools.sessions.secure = ${secure}

+ 

+ # Content Security Policy compatible with default theme

+ csp.default-src = "'none'"

+ csp.script-src = "'self'"

+ csp.style-src = "'self'"

+ csp.img-src = "'self'"

+ csp.font-src = "'self'"

+ csp.form-action = "'self'"

file modified
+1 -11

@@ -11,17 +11,7 @@ 

      <script src="{{ basepath }}/ui/js/bootstrap.js"></script>

      <script src="{{ basepath }}/ui/js/patternfly.js"></script>

      {% if newurl %}

-     <script>

-         $( document ).ready(

-             function() {

-                 history.replaceState({} , document.title, "{{ newurl }}");

-                 $('[data-toggle="tooltip"]').tooltip();

-                 $('[data-toggle="popover"]').popover({

-                   container: 'body'

-                 });

-             }

-         );

-     </script>

+     <script src="{{ basepath }}/ui/js/pages/master-admin.js"></script>

      {% endif %}

      {% block scripts %}

      {% endblock %}

@@ -8,6 +8,6 @@ 

      {% endfor %}

      <button class="btn btn-primary" type="submit">{{ submit }}</button>

    </form>

-   <script>document.getElementById("saml-response").submit();</script>

+   <script src="{{ basepath }}/ui/js/pages/saml2_post_response.js"></script>

  </div>

  {% endblock %}

file modified
+4 -1

@@ -1,4 +1,7 @@ 

  {% extends "master-portal.html" %}

+ {% block scripts %}

+     <script src="{{ basepath }}/ui/js/pages/user_index.js"></script>

+ {% endblock %}

  {% block main %}

  <div class="container">

    <div class="row">

@@ -31,7 +34,7 @@ 

  {%- endfor %}

          </div>

          <div class="col-xs-2">

-             <a id="revoke-{{ item.client }}" class="btn btn-default" href="{{ baseurl }}/consent/revoke/{{ item.provider }}/{{ item.client }}" onclick="return confirm('Do you really want to revoke this consent?');">Revoke</a>

+             <a id="revoke-{{ item.client }}" class="btn btn-default confirm-revoke" href="{{ baseurl }}/consent/revoke/{{ item.provider }}/{{ item.client }}">Revoke</a>

          </div>

        </div>

  {%- endfor %}

@@ -0,0 +1,54 @@ 

+ $( document ).on("click", ".add-field",

+     function() {

+         var buttonRow = $(this).parents(".add-row")

+         var ourTable = $(this).parents(".extensible-table")

+         var lastRow = $(ourTable).find(".list-field:last")

+         var newRow = $(lastRow).clone()

+         lastRow.removeClass("hide")

+         var inputFields = lastRow.find("input")

+         for (i = 0; i < inputFields.length; i++) {

+             $(inputFields[i]).prop("disabled", false)

+         }

+         var lastIndex = parseInt(newRow.find("td:first").text())

+         newRow.find("td:first").text(lastIndex + 1)

+         var inputFields = newRow.find("input")

+         for (i = 0; i < inputFields.length; i++) {

+             var separator = (lastIndex-1).toString()+"-"

+             var nArr = $(inputFields[i]).attr("name").split(separator)

+             var newidx = lastIndex.toString()+"-"

+             if (nArr.length === 2) {

+                 $(inputFields[i]).attr("name", nArr[0]+newidx+nArr[1])

+             }

+             $(inputFields[i]).attr("value", "")

+         }

+         $(newRow).appendTo(ourTable)

+         $(buttonRow).appendTo(ourTable)

+     }

+ );

+ $( document ).on("click", ".image-select",

+     function() {

+         document.getElementById('uploadFile').click();

+     }

+ );

+ $(function() {

+     $("#uploadFile").on("change", function()

+     {

+         var files = !!this.files ? this.files : [];

+         if (!files.length || !window.FileReader) return; // no file selected, or no FileReader support

+ 

+         if (/^image/.test( files[0].type)){ // only image file

+             var reader = new FileReader(); // instance of the FileReader

+             reader.readAsDataURL(files[0]); // read the local file

+ 

+             reader.onloadend = function(e){ // set image data as background of div

+                 var contents = e.target.result;

+                 if (!contents) {

+                     window.alert('Image file is unreadable')

+                     document.getElementById('uploadFile').value = null;

+                 }

+ 

+                 $("#imagePreview").css("background-image", "url("+this.result+")");

+             }

+         }

+     });

+ });

@@ -0,0 +1,5 @@ 

+ $( document ).on("click", ".confirm-delete",

+     function() {

+         return confirm('Do you really want to remove this client?');

+     }

+ ); 

\ No newline at end of file

@@ -0,0 +1,32 @@ 

+ function verifyFile(filename, objid, failtext) {

+     var reader = new FileReader();

+     reader.readAsDataURL(filename); // read the local file

+ 

+     reader.onloadend = function(e){

+         var contents = e.target.result;

+         if (!contents) {

+             window.alert(failtext)

+             document.getElementById(objid).value = null;

+         }

+     }

+ }

+ 

+ $(function() {

+     $("#file").on("change", function()

+     {

+         var files = !!this.files ? this.files : [];

+         if (!files.length || !window.FileReader) return; // no file selected, or no FileReader support

+ 

+         verifyFile(files[0], 'file', 'Metadata file is unreadable');

+     });

+ });

+ 

+ $(function() {

+     $("#image").on("change", function()

+     {

+         var files = !!this.files ? this.files : [];

+         if (!files.length || !window.FileReader) return; // no file selected, or no FileReader support

+ 

+         verifyFile(files[0], 'image', 'Image file is unreadable');

+     });

+ });

file added
+24

@@ -0,0 +1,24 @@ 

+ (function($) {

+   $(document).ready(function() {

+     // Hide the clear button if the search input is empty

+     $(".search-pf .has-clear .clear").each(function() {

+       if (!$(this).prev('.form-control').val()) {

+         $(this).hide();

+       }

+     });

+     // Show the clear button upon entering text in the search input

+     $(".search-pf .has-clear .form-control").keyup(function () {

+       var t = $(this);

+       t.next('button').toggle(Boolean(t.val()));

+     });

+     // Upon clicking the clear button, empty the entered text and hide the clear button

+     $(".search-pf .has-clear .clear").click(function () {

+       $(this).prev('.form-control').val('').focus();

+       $(this).hide();

+     });

+   });    

+   

+   $(function () {

+ $('[data-toggle="tooltip"]').tooltip()

+ })

+ })(jQuery);

@@ -0,0 +1,9 @@ 

+ $( document ).ready(

+     function() {

+         history.replaceState({} , document.title, "{{ newurl }}");

+         $('[data-toggle="tooltip"]').tooltip();

+         $('[data-toggle="popover"]').popover({

+           container: 'body'

+         });

+     }

+ );

@@ -0,0 +1,1 @@ 

+ document.getElementById("saml-response").submit();

@@ -0,0 +1,5 @@ 

+ $( document ).on("click", ".confirm-revoke",

+     function() {

+         return confirm('Do you really want to revoke this consent?');

+     }

+ ); 

\ No newline at end of file