From: Tom Lane Date: Fri, 16 Nov 2001 18:04:31 +0000 (+0000) Subject: plpython security and error handling fixes, from X-Git-Tag: REL7_2_BETA3~48 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=1ca717f377c71ff593d5d944133ef8939c1d4aee;p=postgresql plpython security and error handling fixes, from Kevin Jacobs and Brad McLean. --- 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 ""; + 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';