=== modified file 'NEWS' --- NEWS 2015-09-21 08:03:01 +0000 +++ NEWS 2015-09-21 08:06:49 +0000 @@ -11,6 +11,12 @@ file, and thus triggering a "fill up disk" DoS attack when the .crash report tries to include itself. Thanks to halfdog for discovering this! (CVE-2015-1338, part of LP #1492570) + * SECURITY FIX: Fix all writers of report files (package_hook, + kernel_crashdump, and similar) to open the report file exclusively, i. e. + fail if they already exist. This prevents privilege escalation through + symlink attacks. Note that this will also prevent overwriting previous + reports with the same same. Thanks to halfdog for discovering this! + (CVE-2015-1338, LP: #1492570) 2.18.1 (2015-09-10) ------------------- === modified file 'apport/fileutils.py' --- apport/fileutils.py 2013-11-14 06:54:39 +0000 +++ apport/fileutils.py 2015-09-21 08:06:49 +0000 @@ -9,7 +9,7 @@ # option) any later version. See http://www.gnu.org/copyleft/gpl.html for # the full text of the license. -import os, glob, subprocess, os.path, time, pwd +import os, glob, subprocess, os.path, time, pwd, sys try: from configparser import ConfigParser, NoOptionError, NoSectionError @@ -266,10 +266,14 @@ return 0 -def make_report_path(report, uid=None): - '''Construct a canonical pathname for the given report. +def make_report_file(report, uid=None): + '''Construct a canonical pathname for a report and open it for writing If uid is not given, it defaults to the uid of the current process. + The report file must not exist already, to prevent losing previous reports + or symlink attacks. + + Return an open file object for binary writing. ''' if 'ExecutablePath' in report: subject = report['ExecutablePath'].replace('/', '_') @@ -281,7 +285,11 @@ if not uid: uid = os.getuid() - return os.path.join(report_dir, '%s.%s.crash' % (subject, str(uid))) + path = os.path.join(report_dir, '%s.%s.crash' % (subject, str(uid))) + if sys.version >= '3': + return open(path, 'xb') + else: + return os.fdopen(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640), 'wb') def check_files_md5(sumfile): === modified file 'data/gcc_ice_hook' --- data/gcc_ice_hook 2013-03-27 07:56:05 +0000 +++ data/gcc_ice_hook 2015-09-21 08:06:49 +0000 @@ -31,5 +31,5 @@ pr['PreprocessedSource'] = (sourcefile, False) # write report -with open(apport.fileutils.make_report_path(pr), 'wb') as f: +with apport.fileutils.make_report_file(pr) as f: pr.write(f) === modified file 'data/iwlwifi_error_dump' --- data/iwlwifi_error_dump 2015-09-01 15:33:56 +0000 +++ data/iwlwifi_error_dump 2015-09-21 08:06:49 +0000 @@ -53,7 +53,7 @@ pass try: - with open(apport.fileutils.make_report_path(pr), 'wb') as f: + with apport.fileutils.make_report_file(pr) as f: pr.write(f) except IOError as e: apport.fatal('Cannot create report: ' + str(e)) === modified file 'data/java_uncaught_exception' --- data/java_uncaught_exception 2012-06-15 10:56:50 +0000 +++ data/java_uncaught_exception 2015-09-21 08:06:49 +0000 @@ -85,7 +85,7 @@ report['Title'] = make_title(report) - with open(apport.fileutils.make_report_path(report), 'wb') as f: + with apport.fileutils.make_report_file(report) as f: report.write(f) if __name__ == '__main__': === modified file 'data/kernel_crashdump' --- data/kernel_crashdump 2015-09-21 08:03:01 +0000 +++ data/kernel_crashdump 2015-09-21 08:06:49 +0000 @@ -35,7 +35,7 @@ try: core_fd = os.open(vmcore_path, os.O_RDONLY | os.O_NOFOLLOW) pr['VmCore'] = (os.fdopen(core_fd, 'rb'),) - with open(apport.fileutils.make_report_path(pr), 'wb') as f: + with apport.fileutils.make_report_file(pr) as f: pr.write(f) except (IOError, OSError) as e: apport.fatal('Cannot create report: ' + str(e)) @@ -60,8 +60,8 @@ crash_report = os.path.join(apport.fileutils.report_dir, report_name) dmesg_fd = os.open(dmesg_file, os.O_RDONLY | os.O_NOFOLLOW) pr['VmCoreDmesg'] = (os.fdopen(dmesg_fd, 'rb'),) - if not os.path.exists(crash_report): - with open(crash_report, 'wb') as f: - pr.write(f) + # TODO: Replace with open(..., 'xb') once we drop Python 2 support + with os.fdopen(os.open(crash_report, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640), 'wb') as f: + pr.write(f) except (IOError, OSError) as e: apport.fatal('Cannot create report: ' + str(e)) === modified file 'data/kernel_oops' --- data/kernel_oops 2015-09-01 05:50:54 +0000 +++ data/kernel_oops 2015-09-21 08:06:49 +0000 @@ -37,7 +37,5 @@ pr['Uname'] = '%s %s %s' % (u[0], u[2], u[4]) # write report -report_path = apport.fileutils.make_report_path(pr, uid=checksum) -if not os.path.exists(report_path): - with open(report_path, 'wb') as f: - pr.write(f) +with apport.fileutils.make_report_file(pr, uid=checksum) as f: + pr.write(f) === modified file 'data/package_hook' --- data/package_hook 2015-09-01 05:50:54 +0000 +++ data/package_hook 2015-09-21 08:06:49 +0000 @@ -61,5 +61,5 @@ pr[mkattrname(path)] = (path,) # write report -with open(apport.fileutils.make_report_path(pr), 'wb') as f: +with apport.fileutils.make_report_file(pr) as f: pr.write(f) === modified file 'data/recoverable_problem' --- data/recoverable_problem 2015-04-15 20:30:11 +0000 +++ data/recoverable_problem 2015-09-21 08:06:49 +0000 @@ -67,7 +67,7 @@ report['DuplicateSignature'] = '%s:%s' % (exec_path, ds) # Write the final report - with open(apport.fileutils.make_report_path(report), 'wb') as fp: + with apport.fileutils.make_report_file(report) as fp: report.write(fp) if __name__ == '__main__': === modified file 'data/unkillable_shutdown' --- data/unkillable_shutdown 2012-07-12 15:11:48 +0000 +++ data/unkillable_shutdown 2015-09-21 08:06:49 +0000 @@ -95,7 +95,7 @@ if blacklist: r['OmitPids'] = ' '.join(blacklist) - with open(apport.fileutils.make_report_path(r), 'wb') as f: + with apport.fileutils.make_report_file(r) as f: r.write(f) # === modified file 'test/test_fileutils.py' --- test/test_fileutils.py 2014-07-02 06:31:13 +0000 +++ test/test_fileutils.py 2015-09-21 08:06:49 +0000 @@ -269,16 +269,36 @@ CrashCounter: 3''') self.assertEqual(apport.fileutils.get_recent_crashes(r), 3) - def test_make_report_path(self): - '''make_report_path()''' + def test_make_report_file(self): + '''make_report_file()''' pr = problem_report.ProblemReport() - self.assertRaises(ValueError, apport.fileutils.make_report_path, pr) + self.assertRaises(ValueError, apport.fileutils.make_report_file, pr) pr['Package'] = 'bash 1' - self.assertTrue(apport.fileutils.make_report_path(pr).startswith('%s/bash' % apport.fileutils.report_dir)) + with apport.fileutils.make_report_file(pr) as f: + if sys.version >= '3': + path = f.name + else: + path = os.path.join(apport.fileutils.report_dir, os.listdir(apport.fileutils.report_dir)[0]) + self.assertTrue(path.startswith('%s/bash' % apport.fileutils.report_dir), path) + os.unlink(path) + pr['ExecutablePath'] = '/bin/bash' - self.assertTrue(apport.fileutils.make_report_path(pr).startswith('%s/_bin_bash' % apport.fileutils.report_dir)) + with apport.fileutils.make_report_file(pr) as f: + if sys.version >= '3': + path = f.name + else: + path = os.path.join(apport.fileutils.report_dir, os.listdir(apport.fileutils.report_dir)[0]) + self.assertTrue(path.startswith('%s/_bin_bash' % apport.fileutils.report_dir), path) + + # file exists already, should fail now + self.assertRaises(OSError, apport.fileutils.make_report_file, pr) + + # should still fail if it's a dangling symlink + os.unlink(path) + os.symlink(os.path.join(apport.fileutils.report_dir, 'pwned'), path) + self.assertRaises(OSError, apport.fileutils.make_report_file, pr) def test_check_files_md5(self): '''check_files_md5()'''