summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2021-10-16 22:14:20 +0200
committerFlorian Bruhin <me@the-compiler.org>2021-10-21 16:10:15 +0200
commitca7155d60e4f84f86883b45c6f44e3ce451ed882 (patch)
tree54fe58b0ed7469a1c577fd4fd048f8caba6cc31b
parent53e297ad81c75cb3668232396918eb07e2ce2d4e (diff)
downloadqutebrowser-ca7155d60e4f84f86883b45c6f44e3ce451ed882.tar.gz
qutebrowser-ca7155d60e4f84f86883b45c6f44e3ce451ed882.zip
CVE-2021-41146: Add --untrusted-args to avoid argument injection
On Windows, if an application is registered as an URL handler like this: HKEY_CLASSES_ROOT https URL Protocol = "" [...] shell open command (Default) = ".../qutebrowser.exe" "%1" one would think that Windows takes care of making sure URLs can't inject arguments by containing a quote. However, this is not the case, as stated by the Microsoft docs: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85) Security Warning: Applications that handle URI schemes must consider how to respond to malicious data. Because handler applications can receive data from untrusted sources, the URI and other parameter values passed to the application may contain malicious data that attempts to exploit the handling application. and As noted above, the string that is passed to a pluggable protocol handler might be broken across multiple parameters. Malicious parties could use additional quote or backslash characters to pass additional command line parameters. For this reason, pluggable protocol handlers should assume that any parameters on the command line could come from malicious parties, and carefully validate them. Applications that could initiate dangerous actions based on external data must first confirm those actions with the user. In addition, handling applications should be tested with URIs that are overly long or contain unexpected (or undesirable) character sequences. Indeed it's trivial to pass a command to qutebrowser this way - given how trivial the exploit is to recreate given the information above, here's a PoC: https:x" ":spawn calc (or qutebrowserurl: instead of https: if qutebrowser isn't registered as a default browser) Some applications do escape the quote characters before calling qutebrowser - but others, like Outlook Desktop or .url files, do not. As a fix, we add an --untrusted-args flag and some early validation of the raw sys.argv, before parsing any arguments or e.g. creating a QApplication (which might already allow injecting Qt flags there). We assume that there's no way for an attacker to inject flags *before* the %1 placeholder in the registry, and add --untrusted-args as the last argument of the registry entry. This way, it'd still be possible for users to customize their invocation flags without having to remove --untrusted-args. After --untrusted-args, however, we have some rather strict checks: - There should be zero or one arguments, but not two (or more) - Any argument may not start with - (flag) or : (qutebrowser command) We also add the --untrusted-args flag to the Linux .desktop file, though it should not be needed there, as the specification there is sane: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables Implementations must take care not to expand field codes into multiple arguments unless explicitly instructed by this specification. This means that name fields, filenames and other replacements that can contain spaces must be passed as a single argument to the executable program after expansion. There is no comparable mechanism on macOS, which opens the application without arguments and then sends an "open" event to it: https://doc.qt.io/qt-5/qfileopenevent.html This issue was introduced in qutebrowser v1.7.0 which started registering it as URL handler: baee2888907b260881d5831c68500941937261a0 / #4086 This is by no means an issue isolated to qutebrowser. Many other projects have had similar trouble with Windows' rather unexpected behavior: Electron / Exodus Bitcoin wallet: - http://web.archive.org/web/20190702112128/https://medium.com/0xcc/electrons-bug-shellexecute-to-blame-cacb433d0d62 - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000006 - https://medium.com/hackernoon/exploiting-electron-rce-in-exodus-wallet-d9e6db13c374 IE/Firefox: - https://bugzilla.mozilla.org/show_bug.cgi?id=384384 - https://bugzilla.mozilla.org/show_bug.cgi?id=1572838 Others: - http://web.archive.org/web/20210930203632/https://www.vdoo.com/blog/exploiting-custom-protocol-handlers-in-windows - https://parsiya.net/blog/2021-03-17-attack-surface-analysis-part-2-custom-protocol-handlers/ - etc. etc. See CVE-2021-41146 / GHSA-vw27-fwjf-5qxm: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41146 https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-vw27-fwjf-5qxm Thanks to Ping Fan (Zetta) Ke of Valkyrie-X Security Research Group (VXRL/@vxresearch) for finding and responsibly disclosing this issue. (cherry picked from commit 8f46ba3f6dc7b18375f7aa63c48a1fe461190430) # Conflicts: # qutebrowser/qutebrowser.py # tests/unit/test_qutebrowser.py
-rwxr-xr-xmisc/nsis/install.nsh2
-rw-r--r--misc/org.qutebrowser.qutebrowser.desktop2
-rw-r--r--qutebrowser/qutebrowser.py25
3 files changed, 27 insertions, 2 deletions
diff --git a/misc/nsis/install.nsh b/misc/nsis/install.nsh
index ba932fe07..111b35fc8 100755
--- a/misc/nsis/install.nsh
+++ b/misc/nsis/install.nsh
@@ -351,7 +351,7 @@ Section "Register with Windows" SectionWindowsRegister
!insertmacro UpdateRegDWORD SHCTX "SOFTWARE\Classes\$2" "EditFlags" 0x00000002
!insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\DefaultIcon" "" "$1,0"
!insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell" "" "open"
- !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\command" "" "$\"$1$\" $\"%1$\""
+ !insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\command" "" "$\"$1$\" --untrusted-args $\"%1$\""
!insertmacro UpdateRegStr SHCTX "SOFTWARE\Classes\$2\shell\open\ddeexec" "" ""
StrCmp $2 "${PRODUCT_NAME}HTML" 0 +4
StrCpy $2 "${PRODUCT_NAME}URL"
diff --git a/misc/org.qutebrowser.qutebrowser.desktop b/misc/org.qutebrowser.qutebrowser.desktop
index a1deb319f..20992c1b9 100644
--- a/misc/org.qutebrowser.qutebrowser.desktop
+++ b/misc/org.qutebrowser.qutebrowser.desktop
@@ -44,7 +44,7 @@ Comment[it]= Un browser web vim-like utilizzabile da tastiera basato su PyQt5
Icon=qutebrowser
Type=Application
Categories=Network;WebBrowser;
-Exec=qutebrowser %u
+Exec=qutebrowser --untrusted-args %u
Terminal=false
StartupNotify=false
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute;
diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py
index 05fdd8878..494a0e48e 100644
--- a/qutebrowser/qutebrowser.py
+++ b/qutebrowser/qutebrowser.py
@@ -90,6 +90,11 @@ def get_argparser():
"for more details. This is not needed anymore since "
"Qt 5.11 where the inspector is always enabled and "
"secure.")
+ parser.add_argument('--untrusted-args',
+ action='store_true',
+ help="Mark all following arguments as untrusted, which "
+ "enforces that they are URLs/search terms (and not flags or "
+ "commands)")
parser.add_argument('--json-args', help=argparse.SUPPRESS)
parser.add_argument('--temp-basedir-restarted', help=argparse.SUPPRESS)
@@ -182,7 +187,27 @@ def debug_flag_error(flag):
.format(', '.join(valid_flags)))
+def _validate_untrusted_args(argv):
+ # NOTE: Do not use f-strings here, as this should run with older Python
+ # versions (so that a proper error can be displayed)
+ try:
+ untrusted_idx = argv.index('--untrusted-args')
+ except ValueError:
+ return
+
+ rest = argv[untrusted_idx + 1:]
+ if len(rest) > 1:
+ sys.exit(
+ "Found multiple arguments ({}) after --untrusted-args, "
+ "aborting.".format(' '.join(rest)))
+
+ for arg in rest:
+ if arg.startswith(('-', ':')):
+ sys.exit("Found {} after --untrusted-args, aborting.".format(arg))
+
+
def main():
+ _validate_untrusted_args(sys.argv)
parser = get_argparser()
argv = sys.argv[1:]
args = parser.parse_args(argv)