From 1ca717f377c71ff593d5d944133ef8939c1d4aee Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Fri, 16 Nov 2001 18:04:31 +0000
Subject: [PATCH] plpython security and error handling fixes, from Kevin Jacobs
 and Brad McLean.

---
 src/pl/plpython/error.expected        |  35 +++-
 src/pl/plpython/plpython.c            | 247 +++++++++++++++++++++-----
 src/pl/plpython/plpython_error.sql    |   8 +
 src/pl/plpython/plpython_function.sql |  39 +++-
 4 files changed, 272 insertions(+), 57 deletions(-)

diff --git a/src/pl/plpython/error.expected b/src/pl/plpython/error.expected
index 9c9ac29ddf..96de5da660 100644
--- a/src/pl/plpython/error.expected
+++ b/src/pl/plpython/error.expected
@@ -1,19 +1,36 @@
 SELECT invalid_type_uncaught('rick');
-ERROR:  plpython: Call of function `__plpython_procedure_invalid_type_uncaught_1175341' failed.
+NOTICE:  plpython: in function __plpython_procedure_invalid_type_uncaught_49801:
 plpy.SPIError: Cache lookup for type `test' failed.
 SELECT invalid_type_caught('rick');
-NOTICE:  ("Cache lookup for type `test' failed.",)
- invalid_type_caught 
----------------------
- 
-(1 row)
-
+NOTICE:  plpython: in function __plpython_procedure_invalid_type_caught_49802:
+plpy.SPIError: Cache lookup for type `test' failed.
 SELECT invalid_type_reraised('rick');
-ERROR:  plpython: Call of function `__plpython_procedure_invalid_type_reraised_1175343' failed.
-plpy.Error: ("Cache lookup for type `test' failed.",)
+NOTICE:  plpython: in function __plpython_procedure_invalid_type_reraised_49803:
+plpy.SPIError: Cache lookup for type `test' failed.
 SELECT valid_type('rick');
  valid_type 
 ------------
  
 (1 row)
 
+SELECT read_file('/etc/passwd');
+ERROR:  plpython: Call of function `__plpython_procedure_read_file_49809' failed.
+exceptions.IOError: can't open files in restricted mode
+SELECT write_file('/tmp/plpython','This is very bad');
+ERROR:  plpython: Call of function `__plpython_procedure_write_file_49810' failed.
+exceptions.IOError: can't open files in restricted mode
+SELECT getpid();
+ERROR:  plpython: Call of function `__plpython_procedure_getpid_49811' failed.
+exceptions.AttributeError: getpid
+SELECT uname();
+ERROR:  plpython: Call of function `__plpython_procedure_uname_49812' failed.
+exceptions.AttributeError: uname
+SELECT sys_exit();
+ERROR:  plpython: Call of function `__plpython_procedure_sys_exit_49813' failed.
+exceptions.AttributeError: exit
+SELECT sys_argv();
+    sys_argv    
+----------------
+ ['RESTRICTED']
+(1 row)
+
diff --git a/src/pl/plpython/plpython.c b/src/pl/plpython/plpython.c
index b749d8d5b5..056f01f19e 100644
--- a/src/pl/plpython/plpython.c
+++ b/src/pl/plpython/plpython.c
@@ -29,7 +29,7 @@
  * MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
  *
  * IDENTIFICATION
- *	$Header: /cvsroot/pgsql/src/pl/plpython/plpython.c,v 1.12 2001/11/05 17:46:39 momjian Exp $
+ *	$Header: /cvsroot/pgsql/src/pl/plpython/plpython.c,v 1.13 2001/11/16 18:04:31 tgl Exp $
  *
  *********************************************************************
  */
@@ -188,6 +188,10 @@ static void PLy_init_interp(void);
 static void PLy_init_safe_interp(void);
 static void PLy_init_plpy(void);
 
+/* Helper functions used during initialization */
+static int  populate_methods(PyObject *klass, PyMethodDef *methods);
+static PyObject *build_tuple(char* string_list[], int len);
+
 /* error handler.  collects the current Python exception, if any,
  * and appends it to the error and sends it to elog
  */
@@ -199,6 +203,10 @@ static void
 PLy_exception_set(PyObject *, const char *,...)
 __attribute__((format(printf, 2, 3)));
 
+/* Get the innermost python procedure called from the backend.
+ */
+static char *PLy_procedure_name(PLyProcedure *);
+
 /* some utility functions
  */
 static void *PLy_malloc(size_t);
@@ -240,6 +248,10 @@ static void PLy_input_datum_func2(PLyDatumToOb *, Form_pg_type);
 static void PLy_output_tuple_funcs(PLyTypeInfo *, TupleDesc);
 static void PLy_input_tuple_funcs(PLyTypeInfo *, TupleDesc);
 
+/* RExec methods
+ */
+static PyObject *PLy_r_open(PyObject *self, PyObject* args);
+
 /* conversion functions
  */
 static PyObject *PLyDict_FromTuple(PLyTypeInfo *, HeapTuple, TupleDesc);
@@ -255,6 +267,11 @@ static PyObject *PLyString_FromString(const char *);
 static int	PLy_first_call = 1;
 static volatile int PLy_call_level = 0;
 
+/*
+ * Last function called by postgres backend
+ */
+static PLyProcedure *PLy_last_procedure = NULL;
+
 /* this gets modified in plpython_call_handler and PLy_elog.
  * test it any old where, but do NOT modify it anywhere except
  * those two functions
@@ -265,35 +282,60 @@ static PyObject *PLy_interp_globals = NULL;
 static PyObject *PLy_interp_safe = NULL;
 static PyObject *PLy_interp_safe_globals = NULL;
 static PyObject *PLy_importable_modules = NULL;
+static PyObject *PLy_ok_posix_names = NULL;
+static PyObject *PLy_ok_sys_names = NULL;
 static PyObject *PLy_procedure_cache = NULL;
 
-char	   *PLy_importable_modules_list[] = {
+static char *PLy_importable_modules_list[] = {
 	"array",
 	"bisect",
+	"binascii",
 	"calendar",
 	"cmath",
+	"codecs",
 	"errno",
 	"marshal",
 	"math",
 	"md5",
 	"mpz",
 	"operator",
+	"pcre",
 	"pickle",
 	"random",
 	"re",
+	"regex",
+	"sre",
 	"sha",
 	"string",
 	"StringIO",
+	"struct",
 	"time",
 	"whrandom",
 	"zlib"
 };
 
+static char *PLy_ok_posix_names_list[] = {
+	/* None for now */
+};
+
+static char *PLy_ok_sys_names_list[] = {
+	"byteeorder",
+	"copyright",
+	"getdefaultencoding",
+	"getrefcount",
+	"hexrevision",
+	"maxint",
+	"maxunicode",
+	"platform",
+	"version",
+	"version_info"
+};
+
 /* Python exceptions
  */
-PyObject   *PLy_exc_error = NULL;
-PyObject   *PLy_exc_fatal = NULL;
-PyObject   *PLy_exc_spi_error = NULL;
+static PyObject   *PLy_exc_error = NULL;
+static PyObject   *PLy_exc_fatal = NULL;
+static PyObject   *PLy_exc_spi_error = NULL;
 
 /* some globals for the python module
  */
@@ -334,7 +376,6 @@ perm_fmgr_info(Oid functionId, FmgrInfo *finfo)
 	fmgr_info_cxt(functionId, finfo, TopMemoryContext);
 }
 
-
 Datum
 plpython_call_handler(PG_FUNCTION_ARGS)
 {
@@ -366,8 +407,10 @@ plpython_call_handler(PG_FUNCTION_ARGS)
 		}
 		else
 			PLy_restart_in_progress += 1;
-		if (proc)
+		if (proc) 
+		{
 			Py_DECREF(proc->me);
+		}
 		RERAISE_EXC();
 	}
 
@@ -805,7 +848,7 @@ PLy_function_handler(FunctionCallInfo fcinfo, PLyProcedure * proc)
 	if (plrv == NULL)
 	{
 		elog(FATAL, "Aiieee, PLy_procedure_call returned NULL");
-#if 0
+#ifdef NOT_USED
 		if (!PLy_restart_in_progress)
 			PLy_elog(ERROR, "plpython: Function \"%s\" failed.", proc->proname);
 
@@ -853,11 +896,15 @@ PyObject *
 PLy_procedure_call(PLyProcedure * proc, char *kargs, PyObject * vargs)
 {
 	PyObject   *rv;
+	PLyProcedure *current;
 
 	enter();
 
+	current = PLy_last_procedure;
+	PLy_last_procedure = proc;
 	PyDict_SetItemString(proc->globals, kargs, vargs);
 	rv = PyObject_CallFunction(proc->reval, "O", proc->code);
+	PLy_last_procedure = current;
 
 	if ((rv == NULL) || (PyErr_Occurred()))
 	{
@@ -1150,12 +1197,6 @@ PLy_procedure_compile(PLyProcedure * proc, const char *src)
 	if ((proc->interp == NULL) || (PyErr_Occurred()))
 		PLy_elog(ERROR, "Unable to create rexec.RExec instance");
 
-	/*
-	 * tweak the list of permitted modules
-	 */
-	PyObject_SetAttrString(proc->interp, "ok_builtin_modules",
-						   PLy_importable_modules);
-
 	proc->reval = PyObject_GetAttrString(proc->interp, "r_eval");
 	if ((proc->reval == NULL) || (PyErr_Occurred()))
 		PLy_elog(ERROR, "Unable to get method `r_eval' from rexec.RExec");
@@ -1632,9 +1673,12 @@ static PyObject *PLy_plan_status(PyObject *, PyObject *);
 static PyObject *PLy_result_new(void);
 static void PLy_result_dealloc(PyObject *);
 static PyObject *PLy_result_getattr(PyObject *, char *);
+#ifdef NOT_USED
+/* Appear to be unused */
 static PyObject *PLy_result_fetch(PyObject *, PyObject *);
 static PyObject *PLy_result_nrows(PyObject *, PyObject *);
 static PyObject *PLy_result_status(PyObject *, PyObject *);
+#endif
 static int	PLy_result_length(PyObject *);
 static PyObject *PLy_result_item(PyObject *, int);
 static PyObject *PLy_result_slice(PyObject *, int, int);
@@ -1650,7 +1694,7 @@ static PyObject *PLy_spi_execute_plan(PyObject *, PyObject *, int);
 static PyObject *PLy_spi_execute_fetch_result(SPITupleTable *, int, int);
 
 
-PyTypeObject PLy_PlanType = {
+static PyTypeObject PLy_PlanType = {
 	PyObject_HEAD_INIT(NULL)
 	0,							/* ob_size */
 	"PLyPlan",					/* tp_name */
@@ -1679,13 +1723,13 @@ PyTypeObject PLy_PlanType = {
 	PLy_plan_doc,				/* tp_doc */
 };
 
-PyMethodDef PLy_plan_methods[] = {
+static PyMethodDef PLy_plan_methods[] = {
 	{"status", (PyCFunction) PLy_plan_status, METH_VARARGS, NULL},
 	{NULL, NULL, 0, NULL}
 };
 
 
-PySequenceMethods PLy_result_as_sequence = {
+static PySequenceMethods PLy_result_as_sequence = {
 	(inquiry) PLy_result_length,	/* sq_length */
 	(binaryfunc) 0,				/* sq_concat */
 	(intargfunc) 0,				/* sq_repeat */
@@ -1695,7 +1739,7 @@ PySequenceMethods PLy_result_as_sequence = {
 	(intintobjargproc) PLy_result_ass_slice,	/* sq_ass_slice */
 };
 
-PyTypeObject PLy_ResultType = {
+static PyTypeObject PLy_ResultType = {
 	PyObject_HEAD_INIT(NULL)
 	0,							/* ob_size */
 	"PLyResult",				/* tp_name */
@@ -1723,14 +1767,15 @@ PyTypeObject PLy_ResultType = {
 	0,							/* tp_xxx4 */
 	PLy_result_doc,				/* tp_doc */
 };
-
-PyMethodDef PLy_result_methods[] = {
+#ifdef NOT_USED
+/* Appear to be unused */
+static PyMethodDef PLy_result_methods[] = {
 	{"fetch", (PyCFunction) PLy_result_fetch, METH_VARARGS, NULL,},
 	{"nrows", (PyCFunction) PLy_result_nrows, METH_VARARGS, NULL},
 	{"status", (PyCFunction) PLy_result_status, METH_VARARGS, NULL},
 	{NULL, NULL, 0, NULL}
 };
-
+#endif
 
 static PyMethodDef PLy_methods[] = {
 	/*
@@ -1833,7 +1878,7 @@ PLy_plan_status(PyObject * self, PyObject * args)
 /* result object methods
  */
 
-static PyObject *
+PyObject *
 PLy_result_new(void)
 {
 	PLyResultObject *ob;
@@ -1853,7 +1898,7 @@ PLy_result_new(void)
 	return (PyObject *) ob;
 }
 
-static void
+void
 PLy_result_dealloc(PyObject * arg)
 {
 	PLyResultObject *ob = (PLyResultObject *) arg;
@@ -1867,19 +1912,20 @@ PLy_result_dealloc(PyObject * arg)
 	PyMem_DEL(ob);
 }
 
-static PyObject *
+PyObject *
 PLy_result_getattr(PyObject * self, char *attr)
 {
 	return NULL;
 }
-
-static PyObject *
+#ifdef NOT_USED
+/* Appear to be unused */
+PyObject *
 PLy_result_fetch(PyObject * self, PyObject * args)
 {
 	return NULL;
 }
 
-static PyObject *
+PyObject *
 PLy_result_nrows(PyObject * self, PyObject * args)
 {
 	PLyResultObject *ob = (PLyResultObject *) self;
@@ -1888,7 +1934,7 @@ PLy_result_nrows(PyObject * self, PyObject * args)
 	return ob->nrows;
 }
 
-static PyObject *
+PyObject *
 PLy_result_status(PyObject * self, PyObject * args)
 {
 	PLyResultObject *ob = (PLyResultObject *) self;
@@ -1896,7 +1942,7 @@ PLy_result_status(PyObject * self, PyObject * args)
 	Py_INCREF(ob->status);
 	return ob->status;
 }
-
+#endif
 int
 PLy_result_length(PyObject * arg)
 {
@@ -1991,7 +2037,8 @@ PLy_spi_prepare(PyObject * self, PyObject * args)
 		if (!PyErr_Occurred())
 			PyErr_SetString(PLy_exc_spi_error,
 							"Unknown error in PLy_spi_prepare.");
-		return NULL;
+		PLy_elog(NOTICE,"in function %s:",PLy_procedure_name(PLy_last_procedure));
+		RERAISE_EXC();
 	}
 
 	if (list != NULL)
@@ -2097,7 +2144,7 @@ PLy_spi_execute(PyObject * self, PyObject * args)
 
 	enter();
 
-#if 0
+#ifdef NOT_USED
 
 	/*
 	 * there should - hahaha - be an python exception set so just return
@@ -2187,7 +2234,8 @@ PLy_spi_execute_plan(PyObject * ob, PyObject * list, int limit)
 		if (!PyErr_Occurred())
 			PyErr_SetString(PLy_exc_error,
 							"Unknown error in PLy_spi_execute_plan");
-		return NULL;
+		PLy_elog(NOTICE,"in function %s:",PLy_procedure_name(PLy_last_procedure));
+		RERAISE_EXC();
 	}
 
 	if (nargs)
@@ -2249,16 +2297,15 @@ PLy_spi_execute_query(char *query, int limit)
 	if (TRAP_EXC())
 	{
 		RESTORE_EXC();
-
 		if ((!PLy_restart_in_progress) && (!PyErr_Occurred()))
 			PyErr_SetString(PLy_exc_spi_error,
 							"Unknown error in PLy_spi_execute_query.");
-		return NULL;
+		PLy_elog(NOTICE,"in function %s:",PLy_procedure_name(PLy_last_procedure));
+		RERAISE_EXC();
 	}
 
 	rv = SPI_exec(query, limit);
 	RESTORE_EXC();
-
 	if (rv < 0)
 	{
 		PLy_exception_set(PLy_exc_spi_error,
@@ -2311,7 +2358,7 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, int rows, int status)
 						"Unknown error in PLy_spi_execute_fetch_result");
 			Py_DECREF(result);
 			PLy_typeinfo_dealloc(&args);
-			return NULL;
+			RERAISE_EXC();
 		}
 
 		if (rows)
@@ -2450,13 +2497,33 @@ PLy_init_plpy(void)
 		elog(ERROR, "Unable to init plpy.");
 }
 
+/*
+ *  New RExec methods
+ */
+
+PyObject* 
+PLy_r_open(PyObject *self, PyObject* args)
+{
+	PyErr_SetString(PyExc_IOError, "can't open files in restricted mode");
+	return NULL;
+}
+
+
+static PyMethodDef PLy_r_exec_methods[] = {
+	{"r_open", (PyCFunction)PLy_r_open, METH_VARARGS, NULL},
+	{NULL, NULL, 0, NULL}
+};
+
+/*
+ *  Init new RExec
+ */
+
 void
 PLy_init_safe_interp(void)
 {
-	PyObject   *rmod;
+	PyObject   *rmod, *rexec, *rexec_dict;
 	char	   *rname = "rexec";
-	int			i,
-				imax;
+	int	    len;
 
 	enter();
 
@@ -2467,19 +2534,93 @@ PLy_init_safe_interp(void)
 	PyDict_SetItemString(PLy_interp_globals, rname, rmod);
 	PLy_interp_safe = rmod;
 
-	imax = sizeof(PLy_importable_modules_list) / sizeof(char *);
-	PLy_importable_modules = PyTuple_New(imax);
-	for (i = 0; i < imax; i++)
-	{
-		PyObject   *m = PyString_FromString(PLy_importable_modules_list[i]);
+	len = sizeof(PLy_importable_modules_list) / sizeof(char *);
+	PLy_importable_modules = build_tuple(PLy_importable_modules_list, len);
 
-		PyTuple_SetItem(PLy_importable_modules, i, m);
-	}
+	len = sizeof(PLy_ok_posix_names_list) / sizeof(char *);
+	PLy_ok_posix_names = build_tuple(PLy_ok_posix_names_list, len);
+
+	len = sizeof(PLy_ok_sys_names_list) / sizeof(char *);
+	PLy_ok_sys_names = build_tuple(PLy_ok_sys_names_list, len);
 
 	PLy_interp_safe_globals = PyDict_New();
 	if (PLy_interp_safe_globals == NULL)
 		PLy_elog(ERROR, "Unable to create shared global dictionary.");
 
+	/*
+	 * get an rexec.RExec class
+	 */
+	rexec = PyDict_GetItemString(PyModule_GetDict(rmod), "RExec");
+
+	if (rexec == NULL || !PyClass_Check(rexec))
+		PLy_elog(ERROR, "Unable to get RExec object.");
+
+
+	rexec_dict = ((PyClassObject*)rexec)->cl_dict;
+
+	/*
+	 * tweak the list of permitted modules, posix and sys functions 
+	 */
+	PyDict_SetItemString(rexec_dict, "ok_builtin_modules", PLy_importable_modules);
+	PyDict_SetItemString(rexec_dict, "ok_posix_names",     PLy_ok_posix_names);
+	PyDict_SetItemString(rexec_dict, "ok_sys_names",       PLy_ok_sys_names);
+
+	/*
+	 * change the r_open behavior
+	 */
+	if( populate_methods(rexec, PLy_r_exec_methods) )
+		PLy_elog(ERROR, "Failed to update RExec methods.");
+}
+
+/* Helper function to build tuples from string lists */
+static
+PyObject *build_tuple(char* string_list[], int len)
+{
+	PyObject *tup = PyTuple_New(len);
+	int i;
+	for (i = 0; i < len; i++)
+	{
+		PyObject *m = PyString_FromString(string_list[i]);
+
+		PyTuple_SetItem(tup, i, m);
+	}
+	return tup;
+}
+
+/* Helper function for populating a class with method wrappers. */
+static int
+populate_methods(PyObject *klass, PyMethodDef *methods)
+{
+	if (!klass || !methods)
+		return 0;
+
+	for ( ; methods->ml_name; ++methods) {
+
+		/* get a wrapper for the built-in function */   
+		PyObject *func = PyCFunction_New(methods, NULL);
+		PyObject *meth;
+		int status;
+
+		if (!func)
+			return -1;
+
+		/* turn the function into an unbound method */  
+		if (!(meth = PyMethod_New(func, NULL, klass))) {
+			Py_DECREF(func);
+			return -1;
+		}
+
+		/* add method to dictionary */
+		status = PyDict_SetItemString( ((PyClassObject*)klass)->cl_dict, 
+						methods->ml_name, meth);
+		Py_DECREF(meth);
+		Py_DECREF(func);
+
+		/* stop now if an error occurred, otherwise do the next method */
+		if (status)
+			return status;
+	}
+	return 0;
 }
 
 
@@ -2566,7 +2707,7 @@ PLy_log(volatile int level, PyObject * self, PyObject * args)
 		 * hideously.
 		 */
 		elog(FATAL, "plpython: Aiieee, elog threw an unknown exception!");
-		return NULL;
+		RERAISE_EXC();
 	}
 
 	elog(level, sv);
@@ -2584,6 +2725,18 @@ PLy_log(volatile int level, PyObject * self, PyObject * args)
 }
 
 
+/* Get the last procedure name called by the backend ( the innermost,
+ * If a plpython procedure call calls the backend and the backend calls
+ * another plpython procedure )
+ */
+
+char *PLy_procedure_name(PLyProcedure *proc)
+{
+        if ( proc == NULL )
+	        return "<unknown procedure>";
+        return proc->proname;
+}
+
 /* output a python traceback/exception via the postgresql elog
  * function.  not pretty.
  */
diff --git a/src/pl/plpython/plpython_error.sql b/src/pl/plpython/plpython_error.sql
index 2f0486fed9..0cde4df996 100644
--- a/src/pl/plpython/plpython_error.sql
+++ b/src/pl/plpython/plpython_error.sql
@@ -7,3 +7,11 @@ SELECT invalid_type_uncaught('rick');
 SELECT invalid_type_caught('rick');
 SELECT invalid_type_reraised('rick');
 SELECT valid_type('rick');
+
+-- Security sandbox tests
+SELECT read_file('/etc/passwd');
+SELECT write_file('/tmp/plpython','This is very bad');
+SELECT getpid();
+SELECT uname();
+SELECT sys_exit();
+SELECT sys_argv();
diff --git a/src/pl/plpython/plpython_function.sql b/src/pl/plpython/plpython_function.sql
index bf8bf8bf9f..46083ab2ba 100644
--- a/src/pl/plpython/plpython_function.sql
+++ b/src/pl/plpython/plpython_function.sql
@@ -257,6 +257,12 @@ if len(rv):
 return None
 '
 	LANGUAGE 'plpython';
+/* Flat out syntax error
+*/
+CREATE FUNCTION sql_syntax_error() RETURNS text
+        AS
+'plpy.execute("syntax error")'
+        LANGUAGE 'plpython';
 
 /* check the handling of uncaught python exceptions
  */
@@ -287,5 +293,36 @@ return seq
 '
 	LANGUAGE 'plpython';
 
-
+CREATE OR REPLACE FUNCTION read_file(text) RETURNS text AS '
+  return open(args[0]).read()
+' LANGUAGE 'plpython';
+
+CREATE OR REPLACE FUNCTION write_file(text,text) RETURNS text AS '
+  open(args[0],"w").write(args[1])
+' LANGUAGE 'plpython';
+
+CREATE OR REPLACE FUNCTION getpid() RETURNS int4 AS '
+  import os
+  return os.getpid()
+' LANGUAGE 'plpython';
+
+CREATE OR REPLACE FUNCTION uname() RETURNS int4 AS '
+  import os
+  return os.uname()
+' LANGUAGE 'plpython';
+
+CREATE OR REPLACE FUNCTION sys_exit() RETURNS text AS '
+  import sys
+  return sys.exit()
+' LANGUAGE 'plpython';
+
+CREATE OR REPLACE FUNCTION sys_argv() RETURNS text AS '
+  import sys
+  return str(sys.argv)
+' LANGUAGE 'plpython';
+
+CREATE OR REPLACE FUNCTION sys_version() RETURNS text AS '
+  import sys
+  return str(sys.version)
+' LANGUAGE 'plpython';
 
-- 
2.24.1