# HG changeset patch # User Mads Kiilerich # Date 1577157228 -3600 # Node ID 4f03bd5ac2f2a15004bf131c6538ecb13e0581d2 # Parent c82ef5ec8dcd0da2f69e7b1393c041904111c841 lib: handle both HTML, unsafe strings, and exceptions passed to helpers.flash() Before, h.flash would trust any input to contain html ... and callers would convert exceptions to string, often with a simple str() or unicode() ... which really didn't deserve to be trusted. Instead, only trust messages that have a __html__ and escape anything else ... but also apply str/unicode on the parameter so the caller doesn't have to but *can* pass an exception directly. diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/controllers/admin/repos.py --- a/kallithea/controllers/admin/repos.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/controllers/admin/repos.py Tue Dec 24 04:13:48 2019 +0100 @@ -471,7 +471,7 @@ category='success') except RepositoryError as e: log.error(traceback.format_exc()) - h.flash(str(e), category='error') + h.flash(e, category='error') except Exception as e: log.error(traceback.format_exc()) h.flash(_('An error occurred during this operation'), diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/controllers/changelog.py --- a/kallithea/controllers/changelog.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/controllers/changelog.py Tue Dec 24 04:13:48 2019 +0100 @@ -67,7 +67,7 @@ h.flash(_('There are no changesets yet'), category='error') except RepositoryError as e: log.error(traceback.format_exc()) - h.flash(unicode(e), category='error') + h.flash(e, category='error') raise HTTPBadRequest() @LoginRequired(allow_default_user=True) @@ -111,7 +111,7 @@ cs = self.__get_cs(revision, repo_name) collection = cs.get_file_history(f_path) except RepositoryError as e: - h.flash(unicode(e), category='warning') + h.flash(e, category='warning') raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name)) else: collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision, @@ -125,11 +125,11 @@ c.cs_comments = c.db_repo.get_comments(page_revisions) c.cs_statuses = c.db_repo.statuses(page_revisions) except EmptyRepositoryError as e: - h.flash(unicode(e), category='warning') + h.flash(e, category='warning') raise HTTPFound(location=url('summary_home', repo_name=c.repo_name)) except (RepositoryError, ChangesetDoesNotExistError, Exception) as e: log.error(traceback.format_exc()) - h.flash(unicode(e), category='error') + h.flash(e, category='error') raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name)) c.branch_name = branch_name diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/controllers/files.py --- a/kallithea/controllers/files.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/controllers/files.py Tue Dec 24 04:13:48 2019 +0100 @@ -90,7 +90,7 @@ h.flash(msg, category='error') raise HTTPNotFound() except RepositoryError as e: - h.flash(unicode(e), category='error') + h.flash(e, category='error') raise HTTPNotFound() def __get_filenode(self, cs, path): @@ -110,7 +110,7 @@ h.flash(msg, category='error') raise HTTPNotFound() except RepositoryError as e: - h.flash(unicode(e), category='error') + h.flash(e, category='error') raise HTTPNotFound() return file_node @@ -175,7 +175,7 @@ else: c.authors = c.file_history = [] except RepositoryError as e: - h.flash(unicode(e), category='error') + h.flash(e, category='error') raise HTTPNotFound() if request.environ.get('HTTP_X_PARTIAL_XHR'): diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/controllers/pullrequests.py --- a/kallithea/controllers/pullrequests.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/controllers/pullrequests.py Tue Dec 24 04:13:48 2019 +0100 @@ -340,7 +340,7 @@ try: cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers) except CreatePullRequestAction.ValidationError as e: - h.flash(str(e), category='error', logf=log.error) + h.flash(e, category='error', logf=log.error) raise HTTPNotFound try: @@ -363,7 +363,7 @@ try: cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers) except CreatePullRequestAction.ValidationError as e: - h.flash(str(e), category='error', logf=log.error) + h.flash(e, category='error', logf=log.error) raise HTTPNotFound try: diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/controllers/summary.py --- a/kallithea/controllers/summary.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/controllers/summary.py Tue Dec 24 04:13:48 2019 +0100 @@ -108,7 +108,7 @@ try: collection = c.db_repo_scm_instance.get_changesets(reverse=True) except EmptyRepositoryError as e: - h.flash(unicode(e), category='warning') + h.flash(e, category='warning') collection = [] c.cs_pagination = Page(collection, page=p, items_per_page=size) page_revisions = [x.raw_id for x in list(c.cs_pagination)] diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/lib/base.py --- a/kallithea/lib/base.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/lib/base.py Tue Dec 24 04:13:48 2019 +0100 @@ -604,7 +604,7 @@ raise webob.exc.HTTPNotFound() except RepositoryError as e: log.error(traceback.format_exc()) - h.flash(unicode(e), category='error') + h.flash(e, category='error') raise webob.exc.HTTPBadRequest() diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/lib/helpers.py --- a/kallithea/lib/helpers.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/lib/helpers.py Tue Dec 24 04:13:48 2019 +0100 @@ -423,22 +423,14 @@ Converting the message to a string returns the message text. Instances also have the following attributes: - * ``message``: the message text. * ``category``: the category specified when the message was created. + * ``message``: the html-safe message text. """ def __init__(self, category, message): self.category = category self.message = message - def __str__(self): - return self.message - - __unicode__ = __str__ - - def __html__(self): - return escape(safe_unicode(self.message)) - def _session_flash_messages(append=None, clear=False): """Manage a message queue in tg.session: return the current message queue @@ -460,7 +452,7 @@ return flash_messages -def flash(message, category=None, logf=None): +def flash(message, category, logf=None): """ Show a message to the user _and_ log it through the specified function @@ -470,14 +462,22 @@ logf defaults to log.info, unless category equals 'success', in which case logf defaults to log.debug. """ + assert category in ('error', 'success', 'warning'), category + if hasattr(message, '__html__'): + # render to HTML for storing in cookie + safe_message = unicode(message) + else: + # Apply str - the message might be an exception with __str__ + # Escape, so we can trust the result without further escaping, without any risk of injection + safe_message = html_escape(unicode(message)) if logf is None: logf = log.info if category == 'success': logf = log.debug - logf('Flash %s: %s', category, message) + logf('Flash %s: %s', category, safe_message) - _session_flash_messages(append=(category, message)) + _session_flash_messages(append=(category, safe_message)) def pop_flash_messages(): @@ -485,7 +485,7 @@ The return value is a list of ``Message`` objects. """ - return [_Message(*m) for m in _session_flash_messages(clear=True)] + return [_Message(category, message) for category, message in _session_flash_messages(clear=True)] age = lambda x, y=False: _age(x, y) diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/templates/base/flash_msg.html --- a/kallithea/templates/base/flash_msg.html Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/templates/base/flash_msg.html Tue Dec 24 04:13:48 2019 +0100 @@ -5,7 +5,7 @@ % for message in messages: % endfor % endif diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/tests/functional/test_admin_users.py --- a/kallithea/tests/functional/test_admin_users.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/tests/functional/test_admin_users.py Tue Dec 24 04:13:48 2019 +0100 @@ -203,7 +203,7 @@ .filter(User.username == username).one() response = self.app.post(url('delete_user', id=new_user.user_id), params={'_session_csrf_secret_token': self.session_csrf_secret_token()}) - self.checkSessionFlash(response, 'User "%s" still ' + self.checkSessionFlash(response, 'User "%s" still ' 'owns 1 repositories and cannot be removed. ' 'Switch owners or remove those repositories: ' '%s' % (username, reponame)) @@ -225,7 +225,7 @@ response = self.app.post(url('delete_user', id=new_user.user_id), params={'_session_csrf_secret_token': self.session_csrf_secret_token()}) - self.checkSessionFlash(response, 'User "%s" still ' + self.checkSessionFlash(response, 'User "%s" still ' 'owns 1 repository groups and cannot be removed. ' 'Switch owners or remove those repository groups: ' '%s' % (username, groupname)) @@ -254,7 +254,7 @@ .filter(User.username == username).one() response = self.app.post(url('delete_user', id=new_user.user_id), params={'_session_csrf_secret_token': self.session_csrf_secret_token()}) - self.checkSessionFlash(response, 'User "%s" still ' + self.checkSessionFlash(response, 'User "%s" still ' 'owns 1 user groups and cannot be removed. ' 'Switch owners or remove those user groups: ' '%s' % (username, groupname)) diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/tests/functional/test_files.py --- a/kallithea/tests/functional/test_files.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/tests/functional/test_files.py Tue Dec 24 04:13:48 2019 +0100 @@ -272,7 +272,7 @@ revision=rev, f_path=f_path), status=404) - msg = "There is no file nor directory at the given path: '%s' at revision %s" % (f_path, rev[:12]) + msg = "There is no file nor directory at the given path: '%s' at revision %s" % (f_path, rev[:12]) response.mustcontain(msg) #========================================================================== @@ -308,7 +308,7 @@ repo_name=HG_REPO, revision=rev, f_path=f_path), status=404) - msg = "There is no file nor directory at the given path: '%s' at revision %s" % (f_path, rev[:12]) + msg = "There is no file nor directory at the given path: '%s' at revision %s" % (f_path, rev[:12]) response.mustcontain(msg) def test_ajaxed_files_list(self): diff -r c82ef5ec8dcd -r 4f03bd5ac2f2 kallithea/tests/functional/test_pullrequests.py --- a/kallithea/tests/functional/test_pullrequests.py Thu Dec 26 16:09:30 2019 +0100 +++ b/kallithea/tests/functional/test_pullrequests.py Tue Dec 24 04:13:48 2019 +0100 @@ -171,7 +171,7 @@ 'review_members': [str(invalid_user_id)], }, status=400) - response.mustcontain('Invalid reviewer "%s" specified' % invalid_user_id) + response.mustcontain('Invalid reviewer "%s" specified' % invalid_user_id) def test_edit_with_invalid_reviewer(self): invalid_user_id = 99999 @@ -206,7 +206,7 @@ 'review_members': [str(invalid_user_id)], }, status=400) - response.mustcontain('Invalid reviewer "%s" specified' % invalid_user_id) + response.mustcontain('Invalid reviewer "%s" specified' % invalid_user_id) def test_iteration_refs(self): # Repo graph excerpt: