From f210673b7a6db5034d15ea04c296eef6066f4c6f Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Jul 20 2017 08:01:28 +0000 Subject: [PATCH 1/6] Move all inline javascript to separate files This will allow us to set a very restrictive Content Security Policy for the default template. Signed-off-by: Patrick Uiterwijk --- diff --git a/templates/admin/option_config.html b/templates/admin/option_config.html index 3e0c779..f930ca7 100644 --- a/templates/admin/option_config.html +++ b/templates/admin/option_config.html @@ -1,56 +1,6 @@ {% extends "master-admin.html" %} {% block scripts %} - + {% endblock %} {% block main %} @@ -95,12 +45,11 @@ > {%- endif -%}

- - + id="uploadFile" /> +

{% elif v.__class__.__name__ == 'List' -%} @@ -181,7 +130,7 @@ {% endfor -%} {%- set basename = "%s %d-"|format(v.name, value|length) -%} - + {{value|length + 1}} {%- set basename = "%s %d-"|format(v.name, value|length) -%} - + {{value|length + 1}} +{% endblock %} {% block main %}

Clients

@@ -15,8 +18,7 @@
{{ clients[cid]['client_name'] }} - Delete + Delete
{% endfor %} diff --git a/templates/admin/providers/saml2_sp_new.html b/templates/admin/providers/saml2_sp_new.html index 13f1a9e..59bfda5 100644 --- a/templates/admin/providers/saml2_sp_new.html +++ b/templates/admin/providers/saml2_sp_new.html @@ -70,38 +70,5 @@ - + {% endblock %} diff --git a/templates/index.html b/templates/index.html index 62e36f2..72d7800 100644 --- a/templates/index.html +++ b/templates/index.html @@ -127,33 +127,7 @@ - + diff --git a/templates/master-admin.html b/templates/master-admin.html index 9885036..01494c5 100644 --- a/templates/master-admin.html +++ b/templates/master-admin.html @@ -11,17 +11,7 @@ {% if newurl %} - + {% endif %} {% block scripts %} {% endblock %} diff --git a/templates/saml2/post_response.html b/templates/saml2/post_response.html index 998ac30..ca182d8 100644 --- a/templates/saml2/post_response.html +++ b/templates/saml2/post_response.html @@ -8,6 +8,6 @@ {% endfor %} - + {% endblock %} diff --git a/templates/user/index.html b/templates/user/index.html index a235a22..93a4c50 100644 --- a/templates/user/index.html +++ b/templates/user/index.html @@ -1,4 +1,7 @@ {% extends "master-portal.html" %} +{% block scripts %} + +{% endblock %} {% block main %}
@@ -31,7 +34,7 @@ {%- endfor %}
{%- endfor %} diff --git a/ui/js/pages/admin_option_config.js b/ui/js/pages/admin_option_config.js new file mode 100644 index 0000000..c81705e --- /dev/null +++ b/ui/js/pages/admin_option_config.js @@ -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+")"); + } + } + }); +}); diff --git a/ui/js/pages/admin_providers_openidc.js b/ui/js/pages/admin_providers_openidc.js new file mode 100644 index 0000000..9af584a --- /dev/null +++ b/ui/js/pages/admin_providers_openidc.js @@ -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 diff --git a/ui/js/pages/admin_providers_saml2_sp_new.js b/ui/js/pages/admin_providers_saml2_sp_new.js new file mode 100644 index 0000000..bf6429d --- /dev/null +++ b/ui/js/pages/admin_providers_saml2_sp_new.js @@ -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'); + }); +}); diff --git a/ui/js/pages/index.js b/ui/js/pages/index.js new file mode 100644 index 0000000..3a49b29 --- /dev/null +++ b/ui/js/pages/index.js @@ -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); diff --git a/ui/js/pages/master-admin.js b/ui/js/pages/master-admin.js new file mode 100644 index 0000000..9945a60 --- /dev/null +++ b/ui/js/pages/master-admin.js @@ -0,0 +1,9 @@ +$( document ).ready( + function() { + history.replaceState({} , document.title, "{{ newurl }}"); + $('[data-toggle="tooltip"]').tooltip(); + $('[data-toggle="popover"]').popover({ + container: 'body' + }); + } +); diff --git a/ui/js/pages/saml2_post_response.js b/ui/js/pages/saml2_post_response.js new file mode 100644 index 0000000..cf29c20 --- /dev/null +++ b/ui/js/pages/saml2_post_response.js @@ -0,0 +1 @@ +document.getElementById("saml-response").submit(); diff --git a/ui/js/pages/user_index.js b/ui/js/pages/user_index.js new file mode 100644 index 0000000..b5ffa77 --- /dev/null +++ b/ui/js/pages/user_index.js @@ -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 From 00deec14fde94a852613ab2ff0bd2cab55d77686 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Jul 20 2017 08:01:28 +0000 Subject: [PATCH 2/6] Add system to enable setting of Content Security Policy Signed-off-by: Patrick Uiterwijk --- diff --git a/ipsilon/util/endpoint.py b/ipsilon/util/endpoint.py index 49494fc..4a924dd 100644 --- a/ipsilon/util/endpoint.py +++ b/ipsilon/util/endpoint.py @@ -12,6 +12,39 @@ except ImportError: 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 @@ def allow_iframe(func): @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 @@ class Endpoint(Log): 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 From b2c238df6c1aba3ef4537895a9a1e6f14e7b9684 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Jul 20 2017 08:01:28 +0000 Subject: [PATCH 3/6] Override form-action CSP on IdP return urls Signed-off-by: Patrick Uiterwijk --- diff --git a/ipsilon/providers/openid/auth.py b/ipsilon/providers/openid/auth.py index 8bd907a..d596197 100644 --- a/ipsilon/providers/openid/auth.py +++ b/ipsilon/providers/openid/auth.py @@ -4,6 +4,7 @@ from ipsilon.providers.common import ProviderPageBase 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 @@ class AuthenticateRequest(ProviderPageBase): response.addExtension(resp) return response + @override_csp({'form-action': '*'}) def _respond(self, response): try: self.debug('Response: %s' % response) diff --git a/ipsilon/providers/openidc/auth.py b/ipsilon/providers/openidc/auth.py index ac94196..4308db7 100644 --- a/ipsilon/providers/openidc/auth.py +++ b/ipsilon/providers/openidc/auth.py @@ -9,6 +9,7 @@ from ipsilon.providers.openidc.api import (Token, 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 @@ class AuthenticateRequest(ProviderPageBase): 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) diff --git a/ipsilon/providers/saml2/auth.py b/ipsilon/providers/saml2/auth.py index ba30060..e539728 100644 --- a/ipsilon/providers/saml2/auth.py +++ b/ipsilon/providers/saml2/auth.py @@ -7,6 +7,7 @@ from ipsilon.providers.saml2.provider import InvalidProviderId 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 @@ class AuthenticateRequest(ProviderPageBase): 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 From fb15864da9fe63415214a5a7e9d0160fc6b52f29 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Jul 20 2017 08:01:28 +0000 Subject: [PATCH 4/6] Add default very strict Content Security Policy Signed-off-by: Patrick Uiterwijk --- diff --git a/templates/install/ipsilon.conf b/templates/install/ipsilon.conf index 6388619..fc900de 100644 --- a/templates/install/ipsilon.conf +++ b/templates/install/ipsilon.conf @@ -21,3 +21,11 @@ tools.sessions.path = "${instanceurl}" 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'" From 2e7f304870b07311a2e229e60c1cf503e7f876e6 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Jul 20 2017 08:01:28 +0000 Subject: [PATCH 5/6] Allow inclusion of scheme on admin home Signed-off-by: Patrick Uiterwijk --- diff --git a/ipsilon/admin/common.py b/ipsilon/admin/common.py index f69d11f..3292fce 100644 --- a/ipsilon/admin/common.py +++ b/ipsilon/admin/common.py @@ -4,7 +4,7 @@ import cherrypy 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 @@ class Admin(AdminPage): 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', From a8cdfca4f2b9da2fb3dadf3b6cafadea854fa323 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Jul 20 2017 08:01:28 +0000 Subject: [PATCH 6/6] Allow scheme to use embedded styles Signed-off-by: Patrick Uiterwijk --- diff --git a/ipsilon/admin/common.py b/ipsilon/admin/common.py index 3292fce..782e10d 100644 --- a/ipsilon/admin/common.py +++ b/ipsilon/admin/common.py @@ -325,6 +325,8 @@ class Admin(AdminPage): 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'})