svn commit: r474808 - in branches/2018Q3/www/qutebrowser: . files
Roman Bogorodskiy
novel at FreeBSD.org
Tue Jul 17 17:11:13 UTC 2018
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 at 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);
More information about the svn-ports-branches
mailing list