Restrict a report's CrashDB field to literals Use ast.literal_eval() instead of the generic eval(), to prevent arbitrary code execution from malicious .crash files. A user could be tricked into opening a crash file whose CrashDB field contains an exec(), open(), or similar commands; this is fairly easy as we install a MIME handler for these. Thanks to Donncha O'Cearbhaill for discovering this! CVE-2016-XXXX LP: #1648806 === modified file 'apport/ui.py' --- old/apport/ui.py 2016-12-10 11:28:27 +0000 +++ new/apport/ui.py 2016-12-10 14:03:22 +0000 @@ -18,6 +18,7 @@ import subprocess, threading, webbrowser import signal import time +import ast import apport, apport.fileutils, apport.REThread @@ -920,11 +921,11 @@ # specification? if self.report['CrashDB'].lstrip().startswith('{'): try: - spec = eval(self.report['CrashDB'], {}) + spec = ast.literal_eval(self.report['CrashDB']) assert isinstance(spec, dict) assert 'impl' in spec - except: - self.report['UnreportableReason'] = 'A package hook defines an invalid crash database definition:\n%s' % self.report['CrashDB'] + except Exception as e: + self.report['UnreportableReason'] = 'A package hook defines an invalid crash database definition:\n%s\n%s' % (self.report['CrashDB'], e) return False try: self.crashdb = apport.crashdb.load_crashdb(None, spec) === modified file 'test/test_ui.py' --- old/test/test_ui.py 2016-01-13 07:03:25 +0000 +++ new/test/test_ui.py 2016-12-10 13:45:26 +0000 @@ -441,6 +441,18 @@ self.assertTrue('nonexisting' in self.ui.report['UnreportableReason'], self.ui.report.get('UnreportableReason', '')) + # string with unsafe contents + with open(os.path.join(self.hookdir, 'source_bash.py'), 'w') as f: + f.write('''def add_info(report, ui): + report['CrashDB'] = """{'impl': 'memory', 'trap': exec('open("/tmp/pwned", "w").close()')}""" +''') + + self.ui.report = apport.Report('Bug') + self.ui.cur_package = 'bash' + self.ui.collect_info() + self.assertIn('package hook', self.ui.report['UnreportableReason']) + self.assertFalse(os.path.exists('/tmp/pwned')) + def test_handle_duplicate(self): '''handle_duplicate()''' @@ -922,6 +934,23 @@ (self.ui.msg_title, self.ui.msg_text)) self.assertEqual(self.ui.msg_severity, 'info') + def test_run_crash_malicious_crashdb(self): + '''run_crash() on a crash with malicious CrashDB''' + + self.report['ExecutablePath'] = '/bin/bash' + self.report['Package'] = 'bash 1' + self.report['CrashDB'] = "{'impl': 'memory', 'crash_config': open('/tmp/pwned', 'w').close()}" + self.update_report_file() + self.ui.present_details_response = {'report': True, + 'blacklist': False, + 'examine': False, + 'restart': False} + + self.ui.run_crash(self.report_file.name) + + self.assertFalse(os.path.exists('/tmp/pwned')) + self.assertIn('invalid crash database definition', self.ui.msg_text) + def test_run_crash_ignore(self): '''run_crash() on a crash with the Ignore field''' self.report['Ignore'] = 'True'