Date: Tue, 17 Jul 2018 17:11:12 +0000 (UTC) From: Roman Bogorodskiy <novel@FreeBSD.org> To: ports-committers@freebsd.org, svn-ports-all@freebsd.org, svn-ports-branches@freebsd.org Subject: svn commit: r474808 - in branches/2018Q3/www/qutebrowser: . files Message-ID: <201807171711.w6HHBCQ4059417@repo.freebsd.org>
next in thread | raw e-mail | index | archive | help
Author: novel Date: Tue Jul 17 17:11:12 2018 New Revision: 474808 URL: https://svnweb.freebsd.org/changeset/ports/474808 Log: www/qutebrowser: fix CSRF vulnerability Approved by: ports-secteam (miwi) Obtained from: upstream Security: CVE-2018-10895 Added: branches/2018Q3/www/qutebrowser/files/ branches/2018Q3/www/qutebrowser/files/patch-CVE-2018-10895 (contents, props changed) Modified: branches/2018Q3/www/qutebrowser/Makefile Modified: branches/2018Q3/www/qutebrowser/Makefile ============================================================================== --- branches/2018Q3/www/qutebrowser/Makefile Tue Jul 17 16:53:05 2018 (r474807) +++ branches/2018Q3/www/qutebrowser/Makefile Tue Jul 17 17:11:12 2018 (r474808) @@ -2,6 +2,7 @@ PORTNAME= qutebrowser DISTVERSION= 1.3.3 +PORTREVISION= 1 CATEGORIES= www MASTER_SITES= CHEESESHOP Added: branches/2018Q3/www/qutebrowser/files/patch-CVE-2018-10895 ============================================================================== --- /dev/null 00:00:00 1970 (empty, because file is newly added) +++ branches/2018Q3/www/qutebrowser/files/patch-CVE-2018-10895 Tue Jul 17 17:11:12 2018 (r474808) @@ -0,0 +1,274 @@ +commit c2ff32d92ba9bf40ff53498ee04a4124d4993c85 +Author: Florian Bruhin <git@the-compiler.org> +Date: Mon Jul 9 23:38:47 2018 +0200 + + CVE-2018-10895: Fix CSRF issues with qute://settings/set URL + + In ffc29ee043ae7336d9b9dcc029a05bf7a3f994e8 (part of v1.0.0), a + qute://settings/set URL was added to change settings. + + Contrary to what I apparently believed at the time, it *is* possible for + websites to access `qute://*` URLs (i.e., neither QtWebKit nor QtWebEngine + prohibit such requests, other than the usual cross-origin rules). + + In other words, this means a website can e.g. have an `<img>` tag which loads a + `qute://settings/set` URL, which then sets `editor.command` to a bash script. + The result of that is arbitrary code execution. + + Fixes #4060 + See #2332 + + (cherry picked from commit 43e58ac865ff862c2008c510fc5f7627e10b4660) + +diff --git qutebrowser/browser/qutescheme.py qutebrowser/browser/qutescheme.py +index e3777c389..bd5448ca1 100644 +--- qutebrowser/browser/qutescheme.py ++++ qutebrowser/browser/qutescheme.py +@@ -32,10 +32,18 @@ import textwrap + import mimetypes + import urllib + import collections ++import base64 ++ ++try: ++ import secrets ++except ImportError: ++ # New in Python 3.6 ++ secrets = None + + import pkg_resources + import sip + from PyQt5.QtCore import QUrlQuery, QUrl ++from PyQt5.QtNetwork import QNetworkReply + + import qutebrowser + from qutebrowser.config import config, configdata, configexc, configdiff +@@ -46,6 +54,7 @@ from qutebrowser.misc import objects + + pyeval_output = ":pyeval was never called" + spawn_output = ":spawn was never called" ++csrf_token = None + + + _HANDLERS = {} +@@ -449,12 +458,29 @@ def _qute_settings_set(url): + @add_handler('settings') + def qute_settings(url): + """Handler for qute://settings. View/change qute configuration.""" ++ global csrf_token ++ + if url.path() == '/set': ++ if url.password() != csrf_token: ++ message.error("Invalid CSRF token for qute://settings!") ++ raise QuteSchemeError("Invalid CSRF token!", ++ QNetworkReply.ContentAccessDenied) + return _qute_settings_set(url) + ++ # Requests to qute://settings/set should only be allowed from ++ # qute://settings. As an additional security precaution, we generate a CSRF ++ # token to use here. ++ if secrets: ++ csrf_token = secrets.token_urlsafe() ++ else: ++ # On Python < 3.6, from secrets.py ++ token = base64.urlsafe_b64encode(os.urandom(32)) ++ csrf_token = token.rstrip(b'=').decode('ascii') ++ + src = jinja.render('settings.html', title='settings', + configdata=configdata, +- confget=config.instance.get_str) ++ confget=config.instance.get_str, ++ csrf_token=csrf_token) + return 'text/html', src + + +diff --git qutebrowser/browser/webengine/interceptor.py qutebrowser/browser/webengine/interceptor.py +index 480e8ee85..80563f172 100644 +--- qutebrowser/browser/webengine/interceptor.py ++++ qutebrowser/browser/webengine/interceptor.py +@@ -19,7 +19,9 @@ + + """A request interceptor taking care of adblocking and custom headers.""" + +-from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor ++from PyQt5.QtCore import QUrl ++from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor, ++ QWebEngineUrlRequestInfo) + + from qutebrowser.config import config + from qutebrowser.browser import shared +@@ -54,6 +56,20 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): + Args: + info: QWebEngineUrlRequestInfo &info + """ ++ url = info.requestUrl() ++ firstparty = info.firstPartyUrl() ++ ++ if ((url.scheme(), url.host(), url.path()) == ++ ('qute', 'settings', '/set')): ++ if (firstparty != QUrl('qute://settings/') or ++ info.resourceType() != ++ QWebEngineUrlRequestInfo.ResourceTypeXhr): ++ log.webview.warning("Blocking malicious request from {} to {}" ++ .format(firstparty.toDisplayString(), ++ url.toDisplayString())) ++ info.block(True) ++ return ++ + # FIXME:qtwebengine only block ads for NavigationTypeOther? + if self._host_blocker.is_blocked(info.requestUrl()): + log.webview.info("Request to {} blocked by host blocker.".format( +diff --git qutebrowser/browser/webengine/webenginequtescheme.py qutebrowser/browser/webengine/webenginequtescheme.py +index 12ab6af31..13704ea12 100644 +--- qutebrowser/browser/webengine/webenginequtescheme.py ++++ qutebrowser/browser/webengine/webenginequtescheme.py +@@ -54,8 +54,28 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): + job.fail(QWebEngineUrlRequestJob.UrlInvalid) + return + +- assert job.requestMethod() == b'GET' ++ # Only the browser itself or qute:// pages should access any of those ++ # URLs. ++ # The request interceptor further locks down qute://settings/set. ++ try: ++ initiator = job.initiator() ++ except AttributeError: ++ # Added in Qt 5.11 ++ pass ++ else: ++ if initiator.isValid() and initiator.scheme() != 'qute': ++ log.misc.warning("Blocking malicious request from {} to {}" ++ .format(initiator.toDisplayString(), ++ url.toDisplayString())) ++ job.fail(QWebEngineUrlRequestJob.RequestDenied) ++ return ++ ++ if job.requestMethod() != b'GET': ++ job.fail(QWebEngineUrlRequestJob.RequestDenied) ++ return ++ + assert url.scheme() == 'qute' ++ + log.misc.debug("Got request for {}".format(url.toDisplayString())) + try: + mimetype, data = qutescheme.data_for_url(url) +diff --git qutebrowser/browser/webkit/network/filescheme.py qutebrowser/browser/webkit/network/filescheme.py +index 840ed6a4a..a29674e25 100644 +--- qutebrowser/browser/webkit/network/filescheme.py ++++ qutebrowser/browser/webkit/network/filescheme.py +@@ -111,11 +111,13 @@ def dirbrowser_html(path): + return html.encode('UTF-8', errors='xmlcharrefreplace') + + +-def handler(request): ++def handler(request, _operation, _current_url): + """Handler for a file:// URL. + + Args: + request: QNetworkRequest to answer to. ++ _operation: The HTTP operation being done. ++ _current_url: The page we're on currently. + + Return: + A QNetworkReply for directories, None for files. +diff --git qutebrowser/browser/webkit/network/networkmanager.py qutebrowser/browser/webkit/network/networkmanager.py +index 53508aaa6..a9a591b60 100644 +--- qutebrowser/browser/webkit/network/networkmanager.py ++++ qutebrowser/browser/webkit/network/networkmanager.py +@@ -371,13 +371,6 @@ class NetworkManager(QNetworkAccessManager): + req, proxy_error, QNetworkReply.UnknownProxyError, + self) + +- scheme = req.url().scheme() +- if scheme in self._scheme_handlers: +- result = self._scheme_handlers[scheme](req) +- if result is not None: +- result.setParent(self) +- return result +- + for header, value in shared.custom_headers(): + req.setRawHeader(header, value) + +@@ -406,5 +399,12 @@ class NetworkManager(QNetworkAccessManager): + # the webpage shutdown here. + current_url = QUrl() + ++ scheme = req.url().scheme() ++ if scheme in self._scheme_handlers: ++ result = self._scheme_handlers[scheme](req, op, current_url) ++ if result is not None: ++ result.setParent(self) ++ return result ++ + self.set_referer(req, current_url) + return super().createRequest(op, req, outgoing_data) +diff --git qutebrowser/browser/webkit/network/webkitqutescheme.py qutebrowser/browser/webkit/network/webkitqutescheme.py +index d732b6ab0..b6f99437a 100644 +--- qutebrowser/browser/webkit/network/webkitqutescheme.py ++++ qutebrowser/browser/webkit/network/webkitqutescheme.py +@@ -21,27 +21,46 @@ + + import mimetypes + +-from PyQt5.QtNetwork import QNetworkReply ++from PyQt5.QtCore import QUrl ++from PyQt5.QtNetwork import QNetworkReply, QNetworkAccessManager + + from qutebrowser.browser import pdfjs, qutescheme + from qutebrowser.browser.webkit.network import networkreply + from qutebrowser.utils import log, usertypes, qtutils + + +-def handler(request): ++def handler(request, operation, current_url): + """Scheme handler for qute:// URLs. + + Args: + request: QNetworkRequest to answer to. ++ operation: The HTTP operation being done. ++ current_url: The page we're on currently. + + Return: + A QNetworkReply. + """ ++ if operation != QNetworkAccessManager.GetOperation: ++ return networkreply.ErrorNetworkReply( ++ request, "Unsupported request type", ++ QNetworkReply.ContentOperationNotPermittedError) ++ ++ url = request.url() ++ ++ if ((url.scheme(), url.host(), url.path()) == ++ ('qute', 'settings', '/set')): ++ if current_url != QUrl('qute://settings/'): ++ log.webview.warning("Blocking malicious request from {} to {}" ++ .format(current_url.toDisplayString(), ++ url.toDisplayString())) ++ return networkreply.ErrorNetworkReply( ++ request, "Invalid qute://settings request", ++ QNetworkReply.ContentAccessDenied) ++ + try: +- mimetype, data = qutescheme.data_for_url(request.url()) ++ mimetype, data = qutescheme.data_for_url(url) + except qutescheme.NoHandlerFound: +- errorstr = "No handler found for {}!".format( +- request.url().toDisplayString()) ++ errorstr = "No handler found for {}!".format(url.toDisplayString()) + return networkreply.ErrorNetworkReply( + request, errorstr, QNetworkReply.ContentNotFoundError) + except qutescheme.QuteSchemeOSError as e: +diff --git qutebrowser/html/settings.html qutebrowser/html/settings.html +index 62b424a59..d4ff4ce34 100644 +--- qutebrowser/html/settings.html ++++ qutebrowser/html/settings.html +@@ -3,7 +3,8 @@ + {% block script %} + var cset = function(option, value) { + // FIXME:conf we might want some error handling here? +- var url = "qute://settings/set?option=" + encodeURIComponent(option); ++ var url = "qute://user:{{csrf_token}}@settings/set" ++ url += "?option=" + encodeURIComponent(option); + url += "&value=" + encodeURIComponent(value); + var xhr = new XMLHttpRequest(); + xhr.open("GET", url);
Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201807171711.w6HHBCQ4059417>