Obscure the password on url dsn too

Note that we don't leak anymore the password length.

Fix #528
This commit is contained in:
Daniele Varrazzo 2017-03-16 03:33:19 +00:00
parent 103655d670
commit 9f160fd820
6 changed files with 81 additions and 15 deletions

2
NEWS
View File

@ -7,6 +7,8 @@ What's new in psycopg 2.7.2
- Fixed inconsistent state in externally closed connections - Fixed inconsistent state in externally closed connections
(:tickets:`#263, #311, #443`). Was fixed in 2.6.2 but not included in (:tickets:`#263, #311, #443`). Was fixed in 2.6.2 but not included in
2.7 by mistake. 2.7 by mistake.
- Don't display the password in `connection.dsn` when the connection
string is specified as an URI (:ticket:`#528`).
What's new in psycopg 2.7.1 What's new in psycopg 2.7.1

View File

@ -343,6 +343,9 @@ The ``connection`` class
Read-only string containing the connection string used by the Read-only string containing the connection string used by the
connection. connection.
If a password was specified in the connection string it will be
obscured.
.. index:: .. index::
pair: Transaction; Autocommit pair: Transaction; Autocommit

View File

@ -1248,20 +1248,53 @@ static struct PyGetSetDef connectionObject_getsets[] = {
/* initialization and finalization methods */ /* initialization and finalization methods */
static void RAISES_NEG static int
obscure_password(connectionObject *conn) obscure_password(connectionObject *conn)
{ {
char *pos; PQconninfoOption *options;
PyObject *d = NULL, *v = NULL, *dsn = NULL;
char *tmp;
int rv = -1;
if (!conn || !conn->dsn) { if (!conn || !conn->dsn) {
return; return 0;
} }
pos = strstr(conn->dsn, "password"); if (!(options = PQconninfoParse(conn->dsn, NULL))) {
if (pos != NULL) { /* unlikely: the dsn was already tested valid */
for (pos = pos+9 ; *pos != '\0' && *pos != ' '; pos++) return 0;
*pos = 'x';
} }
if (!(d = psycopg_dict_from_conninfo_options(
options, /* include_password = */ 1))) {
goto exit;
}
if (NULL == PyDict_GetItemString(d, "password")) {
/* the dsn doesn't have a password */
rv = 0;
goto exit;
}
/* scrub the password and put back the connection string together */
if (!(v = Text_FromUTF8("xxx"))) { goto exit; }
if (0 > PyDict_SetItemString(d, "password", v)) { goto exit; }
if (!(dsn = psycopg_make_dsn(Py_None, d))) { goto exit; }
if (!(dsn = psycopg_ensure_bytes(dsn))) { goto exit; }
/* Replace the connection string on the connection object */
tmp = conn->dsn;
psycopg_strdup(&conn->dsn, Bytes_AS_STRING(dsn), -1);
PyMem_Free(tmp);
rv = 0;
exit:
PQconninfoFree(options);
Py_XDECREF(v);
Py_XDECREF(d);
Py_XDECREF(dsn);
return rv;
} }
static int static int
@ -1303,7 +1336,12 @@ connection_setup(connectionObject *self, const char *dsn, long int async)
exit: exit:
/* here we obfuscate the password even if there was a connection error */ /* here we obfuscate the password even if there was a connection error */
{
PyObject *ptype = NULL, *pvalue = NULL, *ptb = NULL;
PyErr_Fetch(&ptype, &pvalue, &ptb);
obscure_password(self); obscure_password(self);
PyErr_Restore(ptype, pvalue, ptb);
}
return res; return res;
} }

View File

@ -142,6 +142,8 @@ STEALS(1) HIDDEN PyObject * psycopg_ensure_text(PyObject *obj);
HIDDEN PyObject *psycopg_dict_from_conninfo_options(PQconninfoOption *options, HIDDEN PyObject *psycopg_dict_from_conninfo_options(PQconninfoOption *options,
int include_password); int include_password);
HIDDEN PyObject *psycopg_make_dsn(PyObject *dsn, PyObject *kwargs);
/* Exceptions docstrings */ /* Exceptions docstrings */
#define Error_doc \ #define Error_doc \
"Base class for error exceptions." "Base class for error exceptions."

View File

@ -280,6 +280,30 @@ exit:
} }
/* Make a connection string out of a string and a dictionary of arguments.
*
* Helper to call psycopg2.extensions.make_dns()
*/
PyObject *
psycopg_make_dsn(PyObject *dsn, PyObject *kwargs)
{
PyObject *ext = NULL, *make_dsn = NULL;
PyObject *args = NULL, *rv = NULL;
if (!(ext = PyImport_ImportModule("psycopg2.extensions"))) { goto exit; }
if (!(make_dsn = PyObject_GetAttrString(ext, "make_dsn"))) { goto exit; }
if (!(args = PyTuple_Pack(1, dsn))) { goto exit; }
rv = PyObject_Call(make_dsn, args, kwargs);
exit:
Py_XDECREF(args);
Py_XDECREF(make_dsn);
Py_XDECREF(ext);
return rv;
}
/* Convert a C string into Python Text using a specified codec. /* Convert a C string into Python Text using a specified codec.
* *
* The codec is the python function codec.getdecoder(enc). It is only used on * The codec is the python function codec.getdecoder(enc). It is only used on

View File

@ -1504,19 +1504,16 @@ class PasswordLeakTestCase(ConnectingTestCase):
def test_leak(self): def test_leak(self):
self.assertRaises(psycopg2.DatabaseError, self.assertRaises(psycopg2.DatabaseError,
self.GrassingConnection, "dbname=nosuch password=whateva") self.GrassingConnection, "dbname=nosuch password=whateva")
self.assertDsnEqual(self.dsn, "dbname=nosuch password=xxx")
self.assert_('nosuch' in self.dsn) @skip_before_libpq(9, 2)
self.assert_('password' in self.dsn)
self.assert_('whateva' not in self.dsn)
def test_url_leak(self): def test_url_leak(self):
self.assertRaises(psycopg2.DatabaseError, self.assertRaises(psycopg2.DatabaseError,
self.GrassingConnection, self.GrassingConnection,
"postgres://someone:whateva@localhost/nosuch") "postgres://someone:whateva@localhost/nosuch")
self.assert_('nosuch' in self.dsn) self.assertDsnEqual(self.dsn,
self.assert_('someone' in self.dsn) "user=someone password=xxx host=localhost dbname=nosuch")
self.assert_('whateva' not in self.dsn)
def test_suite(): def test_suite():