diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index b78654e..0722cdb 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -54,6 +54,7 @@ from IPython.core.profiledir import ProfileDir from IPython.core.prompts import PromptManager from IPython.core.usage import default_banner from IPython.lib.latextools import LaTeXTool +from IPython.lib.security import InputRejected from IPython.testing.skipdoctest import skip_doctest from IPython.utils import PyColorize from IPython.utils import io @@ -2786,7 +2787,13 @@ class InteractiveShell(SingletonConfigurable): return None # Apply AST transformations - code_ast = self.transform_ast(code_ast) + try: + code_ast = self.transform_ast(code_ast) + except InputRejected: + self.showtraceback() + if store_history: + self.execution_count += 1 + return None # Execute the user code interactivity = "none" if silent else self.ast_node_interactivity @@ -2822,6 +2829,11 @@ class InteractiveShell(SingletonConfigurable): for transformer in self.ast_transformers: try: node = transformer.visit(node) + except InputRejected: + # User-supplied AST transformers can reject an input by raising + # an InputRejected. Short-circuit in this case so that we + # don't unregister the transform. + raise except Exception: warn("AST transformer %r threw an error. It will be unregistered." % transformer) self.ast_transformers.remove(transformer) diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index cc3ce21..326a84b 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -25,6 +25,7 @@ from os.path import join import nose.tools as nt from IPython.core.inputtransformer import InputTransformer +from IPython.lib.security import InputRejected from IPython.testing.decorators import ( skipif, skip_win32, onlyif_unicode_paths, onlyif_cmds_exist, ) @@ -695,6 +696,41 @@ class TestAstTransformError(unittest.TestCase): # This should have been removed. nt.assert_not_in(err_transformer, ip.ast_transformers) + +class StringRejector(ast.NodeTransformer): + """Throws an InputRejected when it sees a string literal. + + Used to verify that NodeTransformers can signal that a piece of code should + not be executed by throwing an InputRejected. + """ + + def visit_Str(self, node): + raise InputRejected("test") + + +class TestAstTransformInputRejection(unittest.TestCase): + + def setUp(self): + self.transformer = StringRejector() + ip.ast_transformers.append(self.transformer) + + def tearDown(self): + ip.ast_transformers.remove(self.transformer) + + def test_input_rejection(self): + """Check that NodeTransformers can reject input.""" + + expect_exception_tb = tt.AssertPrints("InputRejected: test") + expect_no_cell_output = tt.AssertNotPrints("'unsafe'", suppress=False) + + # Run the same check twice to verify that the transformer is not + # disabled after raising. + with expect_exception_tb, expect_no_cell_output: + ip.run_cell("'unsafe'") + + with expect_exception_tb, expect_no_cell_output: + ip.run_cell("'unsafe'") + def test__IPYTHON__(): # This shouldn't raise a NameError, that's all __IPYTHON__ diff --git a/IPython/lib/security.py b/IPython/lib/security.py index eab67bb..564410d 100644 --- a/IPython/lib/security.py +++ b/IPython/lib/security.py @@ -114,3 +114,13 @@ def passwd_check(hashed_passphrase, passphrase): h.update(cast_bytes(passphrase, 'utf-8') + cast_bytes(salt, 'ascii')) return h.hexdigest() == pw_digest + +#----------------------------------------------------------------------------- +# Exception classes +#----------------------------------------------------------------------------- +class InputRejected(Exception): + """Input rejected by ast transformer. + + To be raised by user-supplied ast.NodeTransformers when an input should not + be executed. + """