diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py index 106803683..03160ef86 100644 --- a/tests/test_form_pages.py +++ b/tests/test_form_pages.py @@ -428,6 +428,47 @@ def test_form_access_auth_context(pub): assert resp.form +def test_form_cancelurl(pub): + formdef = create_formdef() + formdef.data_class().wipe() + + # path + resp = get_app(pub).get('/test/?cancelurl=/plop/') + resp = resp.form.submit('cancel') + assert resp.location == 'http://example.net/plop/' + + # full URL + resp = get_app(pub).get('/test/?cancelurl=http://example.net/plop/') + resp = resp.form.submit('cancel') + assert resp.location == 'http://example.net/plop/' + + # remote site + get_app(pub).get('/test/?cancelurl=http://example.org/plop/', status=400) + + pub.site_options.add_section('api-secrets') + pub.site_options.set('api-secrets', 'example.org', 'xyz') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + resp = get_app(pub).get('/test/?cancelurl=http://example.org/plop/') + resp = resp.form.submit('cancel') + assert resp.location == 'http://example.org/plop/' + + pub.site_options.remove_section('api-secrets') + if not pub.site_options.has_section('options'): + pub.site_options.add_section('options') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + get_app(pub).get('/test/?cancelurl=http://example.org/plop/', status=400) + + pub.site_options.set('options', 'relatable-hosts', 'example.com') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + get_app(pub).get('/test/?cancelurl=http://example.org/plop/', status=400) + + pub.site_options.set('options', 'relatable-hosts', 'example.com, example.org') + pub.site_options.write(open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w')) + resp = get_app(pub).get('/test/?cancelurl=http://example.org/plop/') + resp = resp.form.submit('cancel') + assert resp.location == 'http://example.org/plop/' + + def test_form_submit(pub): formdef = create_formdef() formdef.data_class().wipe() diff --git a/wcs/forms/root.py b/wcs/forms/root.py index af9bc4672..a5abf3ada 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -328,6 +328,8 @@ class FormPage(Directory, FormTemplateMixin): if page == self.pages[0] and 'cancelurl' in get_request().form: cancelurl = get_request().form['cancelurl'] + if not get_publisher().is_relatable_url(cancelurl): + raise errors.RequestError('invalid cancel URL') form_data['__cancelurl'] = cancelurl session.add_magictoken(magictoken, form_data) diff --git a/wcs/qommon/publisher.py b/wcs/qommon/publisher.py index b2bfd97a8..58c637437 100644 --- a/wcs/qommon/publisher.py +++ b/wcs/qommon/publisher.py @@ -386,6 +386,7 @@ class QommonPublisher(Publisher, object): defaults = { 'options': { 'unused-files-behaviour': 'remove', + 'relatable-hosts': '', }, } if self.site_options is None: @@ -956,6 +957,23 @@ class QommonPublisher(Publisher, object): d['manager_homepage_title'] = d.get('portal_agent_title') return d + def is_relatable_url(self, url): + parsed_url = urllib.urlparse(url) + if not parsed_url.netloc: + return True + if parsed_url.netloc == urllib.urlparse(self.get_frontoffice_url()).netloc: + return True + if parsed_url.netloc == urllib.urlparse(self.get_backoffice_url()).netloc: + return True + if parsed_url.netloc in [x.strip() for x in self.get_site_option('relatable-hosts').split(',')]: + return True + try: + if parsed_url.netloc in self.site_options.options('api-secrets'): + return True + except ConfigParser.NoSectionError: + pass + return False + extra_sources = [] @classmethod def register_extra_source(cls, source):