Skip site navigation (1)Skip section navigation (2)
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>