diff -Nru apport-2.20.9/apport/ui.py apport-2.20.9/apport/ui.py --- apport-2.20.9/apport/ui.py 2020-07-24 15:08:40.000000000 +0200 +++ apport-2.20.9/apport/ui.py 2023-04-12 19:53:49.000000000 +0200 @@ -24,6 +24,7 @@ import apport.crashdb from apport import unicode_gettext as _ +from apport.user_group import get_process_user_and_group if sys.version_info.major == 2: from ConfigParser import ConfigParser @@ -57,6 +58,33 @@ return None +def _get_env_int(key, default=None): + """Get an environment variable as integer. + + Return None if it doesn't exist or failed to convert to integer. + The optional second argument can specify an alternate default. + """ + try: + return int(os.environ[key]) + except (KeyError, ValueError): + return default + + +def run_as_real_user(args, **kwargs): + """Call subprocess.run as real user if called via sudo/pkexec. + + If we are called through pkexec/sudo, determine the real user ID and + run the command with it to get the user's web browser settings. + """ + uid = _get_env_int("SUDO_UID", _get_env_int("PKEXEC_UID")) + if uid is None or not get_process_user_and_group().is_root(): + subprocess.run(args, check=False, **kwargs) + return + + sudo_prefix = ['sudo', '-H', '-u', '#' + str(uid)] + subprocess.run(sudo_prefix + args, check=False, **kwargs) + + def still_running(pid): try: os.kill(int(pid), 0) diff -Nru apport-2.20.9/apport/user_group.py apport-2.20.9/apport/user_group.py --- apport-2.20.9/apport/user_group.py 1970-01-01 01:00:00.000000000 +0100 +++ apport-2.20.9/apport/user_group.py 2023-04-12 19:53:49.000000000 +0200 @@ -0,0 +1,24 @@ +# Copyright (C) 2023 Canonical Ltd. +# Author: Benjamin Drung +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Functions around users and groups.""" + +import os + + +class UserGroupID: + """Pair of user and group ID.""" + + def __init__(self, uid, gid): + self.uid = uid + self.gid = gid + + def is_root(self): + """Check if the user or group ID is root.""" + return self.uid == 0 or self.gid == 0 + + +def get_process_user_and_group(): + """Return the current process's real user and group.""" + return UserGroupID(os.getuid(), os.getgid()) diff -Nru apport-2.20.9/backends/packaging-apt-dpkg.py apport-2.20.9/backends/packaging-apt-dpkg.py --- apport-2.20.9/backends/packaging-apt-dpkg.py 2021-05-13 13:33:16.000000000 +0200 +++ apport-2.20.9/backends/packaging-apt-dpkg.py 2023-04-12 19:53:49.000000000 +0200 @@ -233,6 +233,8 @@ return False def get_lp_binary_package(self, distro_id, package, version, arch): + # allow unauthenticated downloads + apt.apt_pkg.config.set('APT::Get::AllowUnauthenticated', 'True') package = quote(package) version = quote(version) ma = self.json_request(self._archive_url % distro_id) diff -Nru apport-2.20.9/bin/apport-cli apport-2.20.9/bin/apport-cli --- apport-2.20.9/bin/apport-cli 2019-11-11 22:57:56.000000000 +0100 +++ apport-2.20.9/bin/apport-cli 2023-04-12 19:53:49.000000000 +0200 @@ -176,8 +176,9 @@ self.in_update_view = True report = self._get_details() try: - p = subprocess.Popen(['/usr/bin/sensible-pager'], stdin=subprocess.PIPE) - p.communicate(report.encode('UTF-8')) + apport.ui.run_as_real_user( + ["/usr/bin/sensible-pager"], input=report.encode("UTF-8") + ) except IOError as e: # ignore broken pipe (premature quit) if e.errno == errno.EPIPE: diff -Nru apport-2.20.9/.bzr-builddeb/default.conf apport-2.20.9/.bzr-builddeb/default.conf --- apport-2.20.9/.bzr-builddeb/default.conf 1970-01-01 01:00:00.000000000 +0100 +++ apport-2.20.9/.bzr-builddeb/default.conf 2023-04-12 19:53:49.000000000 +0200 @@ -0,0 +1,2 @@ +[BUILDDEB] +merge = True diff -Nru apport-2.20.9/debian/changelog apport-2.20.9/debian/changelog --- apport-2.20.9/debian/changelog 2022-05-10 15:23:35.000000000 +0200 +++ apport-2.20.9/debian/changelog 2023-04-12 19:53:49.000000000 +0200 @@ -1,3 +1,16 @@ +apport (2.20.9-0ubuntu7.29) bionic-security; urgency=medium + + * SECURITY UPDATE: viewing an apport-cli crash with default pager could + escalate privilege (LP: #2016023) + - apport/ui.py, apport/user_group.py, bin/apport-cli: drops privilege to + users environment before execution (using sudo) + - test/test_ui.py, test/test_user/group.py: Add test cases for new code + - CVE-2023-1326 + * backends/packaging-apt-dpkg.py: when downloading packages from Launchpad + do not require them to be authenticated. (LP: #1989467) + + -- Benjamin Drung Wed, 12 Apr 2023 19:53:49 +0200 + apport (2.20.9-0ubuntu7.28) bionic-security; urgency=medium * SECURITY UPDATE: Fix multiple security issues diff -Nru apport-2.20.9/test/test_ui.py apport-2.20.9/test/test_ui.py --- apport-2.20.9/test/test_ui.py 2021-10-18 13:48:31.000000000 +0200 +++ apport-2.20.9/test/test_ui.py 2023-04-12 19:53:49.000000000 +0200 @@ -1,6 +1,7 @@ # coding: UTF-8 import unittest, shutil, signal, tempfile, resource, pwd, time, os, sys, imp import subprocess, errno, glob +import unittest.mock try: from cStringIO import StringIO @@ -10,7 +11,7 @@ from io import BytesIO import apport.ui -from apport.ui import _ +from apport.ui import _, run_as_real_user import apport.report import problem_report import apport.crashdb_impl.memory @@ -2373,5 +2374,41 @@ os.waitpid(pid, 0) self.ui.wait_for_pid(pid) + @unittest.mock.patch("os.getgid", unittest.mock.MagicMock(return_value=0)) + @unittest.mock.patch("os.getuid", unittest.mock.MagicMock(return_value=0)) + @unittest.mock.patch.dict( + "os.environ", {"SUDO_UID": str(os.getuid())}, clear=True + ) + def test_run_as_real_user(self): + # pylint: disable=no-self-use + """Test run_as_real_user() with SUDO_UID set.""" + with unittest.mock.patch("subprocess.run") as run_mock: + run_as_real_user(["/bin/true"]) + + run_mock.assert_called_once_with( + ["sudo", "-H", "-u", "#" + os.environ["SUDO_UID"], "/bin/true"], + check=False, + ) + + @unittest.mock.patch.dict("os.environ", {}) + def test_run_as_real_user_no_sudo(self): + # pylint: disable=no-self-use + """Test run_as_real_user() without sudo env variables.""" + with unittest.mock.patch("subprocess.run") as run_mock: + run_as_real_user(["/bin/true"]) + + run_mock.assert_called_once_with(["/bin/true"], check=False) + + @unittest.mock.patch("os.getgid", unittest.mock.MagicMock(return_value=37)) + @unittest.mock.patch("os.getuid", unittest.mock.MagicMock(return_value=37)) + @unittest.mock.patch.dict("os.environ", {"SUDO_UID": "0"}) + def test_run_as_real_user_non_root(self): + # pylint: disable=no-self-use + """Test run_as_real_user() as non-root and SUDO_UID set.""" + with unittest.mock.patch("subprocess.run") as run_mock: + run_as_real_user(["/bin/true"]) + + run_mock.assert_called_once_with(["/bin/true"], check=False) + unittest.main() diff -Nru apport-2.20.9/test/test_user_group.py apport-2.20.9/test/test_user_group.py --- apport-2.20.9/test/test_user_group.py 1970-01-01 01:00:00.000000000 +0100 +++ apport-2.20.9/test/test_user_group.py 2023-04-12 19:53:49.000000000 +0200 @@ -0,0 +1,25 @@ +# Copyright (C) 2023 Canonical Ltd. +# Author: Benjamin Drung +# SPDX-License-Identifier: GPL-2.0-or-later + +"""Unit tests for apport.user_group.""" + +import unittest +from unittest.mock import MagicMock, patch + +from apport.user_group import get_process_user_and_group + + +class TestUserGroup(unittest.TestCase): + # pylint: disable=missing-function-docstring + """Unit tests for apport.user_group.""" + + @patch("os.getgid", MagicMock(return_value=0)) + @patch("os.getuid", MagicMock(return_value=0)) + def test_get_process_user_and_group_is_root(self): + self.assertTrue(get_process_user_and_group().is_root()) + + @patch("os.getgid", MagicMock(return_value=2000)) + @patch("os.getuid", MagicMock(return_value=3000)) + def test_get_process_user_and_group_is_not_root(self): + self.assertFalse(get_process_user_and_group().is_root())