Skip to content

Commit c72ee15

Browse files
committed
PYTHON-1282 Username/password must be URI-escaped
Not just "%", "@", and ":" must be escaped, all delimiters from RFC 3986 must be percent-encoded. Therefore, since "/" must be escaped in username and password, we can split the URI at the first "/" instead of the last.
1 parent 803c83d commit c72ee15

File tree

11 files changed

+607
-473
lines changed

11 files changed

+607
-473
lines changed

doc/examples/authentication.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,22 @@ MongoDB supports several different authentication mechanisms. These examples
55
cover all authentication methods currently supported by PyMongo, documenting
66
Python module and MongoDB version dependencies.
77

8-
Support For Special Characters In Usernames And Passwords
9-
---------------------------------------------------------
8+
Percent-Escaping Username and Password
9+
--------------------------------------
1010

11-
If your username or password contains special characters (e.g. '/', ' ',
12-
or '@') you must ``%xx`` escape them for use in the MongoDB URI. PyMongo
13-
uses :meth:`~urllib.unquote_plus` to decode them. For example::
11+
Username and password must be percent-escaped with :meth:`~urllib.quote_plus`
12+
to be used in a MongoDB URI. PyMongo uses :meth:`~urllib.unquote_plus`
13+
internally to decode them. For example::
1414

1515
>>> from pymongo import MongoClient
1616
>>> import urllib
17+
>>> username = urllib.quote_plus('user')
18+
>>> username
19+
'user'
1720
>>> password = urllib.quote_plus('pass/word')
1821
>>> password
1922
'pass%2Fword'
20-
>>> MongoClient('mongodb://user:' + password + '@127.0.0.1')
23+
>>> MongoClient('mongodb://%s:%s@127.0.0.1' % (username, password)
2124
MongoClient('127.0.0.1', 27017)
2225

2326
SCRAM-SHA-1 (RFC 5802)

pymongo/uri_parser.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515

1616
"""Tools to parse and validate a MongoDB URI."""
17+
import re
1718
import warnings
1819

1920
from bson.py3compat import PY3, iteritems, string_type
@@ -68,7 +69,7 @@ def _rpartition(entity, sep):
6869
def parse_userinfo(userinfo):
6970
"""Validates the format of user information in a MongoDB URI.
7071
Reserved characters like ':', '/', '+' and '@' must be escaped
71-
following RFC 2396.
72+
following RFC 3986.
7273
7374
Returns a 2-tuple containing the unescaped username followed
7475
by the unescaped password.
@@ -80,16 +81,13 @@ def parse_userinfo(userinfo):
8081
Now uses `urllib.unquote_plus` so `+` characters must be escaped.
8182
"""
8283
if '@' in userinfo or userinfo.count(':') > 1:
83-
raise InvalidURI("':' or '@' characters in a username or password "
84-
"must be escaped according to RFC 2396.")
84+
raise InvalidURI("Username and password must be escaped according to "
85+
"RFC 3986, use urllib.quote_plus().")
8586
user, _, passwd = _partition(userinfo, ":")
8687
# No password is expected with GSSAPI authentication.
8788
if not user:
8889
raise InvalidURI("The empty string is not valid username.")
89-
user = unquote_plus(user)
90-
passwd = unquote_plus(passwd)
91-
92-
return user, passwd
90+
return unquote_plus(user), unquote_plus(passwd)
9391

9492

9593
def parse_ipv6_literal_host(entity, default_port):
@@ -251,6 +249,11 @@ def split_hosts(hosts, default_port=DEFAULT_PORT):
251249
return nodes
252250

253251

252+
# Prohibited characters in database name. DB names also can't have ".", but for
253+
# backward-compat we allow "db.collection" in URI.
254+
_BAD_DB_CHARS = re.compile('[' + re.escape(r'/ "$') + ']')
255+
256+
254257
def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False):
255258
"""Parse and validate a MongoDB URI.
256259
@@ -294,19 +297,10 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False):
294297
collection = None
295298
options = {}
296299

297-
# Check for unix domain sockets in the uri
298-
if '.sock' in scheme_free:
299-
host_part, _, path_part = _rpartition(scheme_free, '/')
300-
if not host_part:
301-
host_part = path_part
302-
path_part = ""
303-
if '/' in host_part:
304-
raise InvalidURI("Any '/' in a unix domain socket must be"
305-
" URL encoded: %s" % host_part)
306-
host_part = unquote_plus(host_part)
307-
path_part = unquote_plus(path_part)
308-
else:
309-
host_part, _, path_part = _partition(scheme_free, '/')
300+
host_part, _, path_part = _partition(scheme_free, '/')
301+
if not host_part:
302+
host_part = path_part
303+
path_part = ""
310304

311305
if not path_part and '?' in host_part:
312306
raise InvalidURI("A '/' is required between "
@@ -318,17 +312,24 @@ def parse_uri(uri, default_port=DEFAULT_PORT, validate=True, warn=False):
318312
else:
319313
hosts = host_part
320314

315+
if '/' in hosts:
316+
raise InvalidURI("Any '/' in a unix domain socket must be"
317+
" URL encoded: %s" % host_part)
318+
319+
hosts = unquote_plus(hosts)
321320
nodes = split_hosts(hosts, default_port=default_port)
322321

323322
if path_part:
324-
325323
if path_part[0] == '?':
326-
opts = path_part[1:]
324+
opts = unquote_plus(path_part[1:])
327325
else:
328-
dbase, _, opts = _partition(path_part, '?')
326+
dbase, _, opts = map(unquote_plus, _partition(path_part, '?'))
329327
if '.' in dbase:
330328
dbase, collection = dbase.split('.', 1)
331329

330+
if _BAD_DB_CHARS.search(dbase):
331+
raise InvalidURI('Bad database name "%s"' % dbase)
332+
332333
if opts:
333334
options = split_options(opts, validate, warn)
334335

test/connection_string/test/invalid-uris.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@
189189
"valid": false,
190190
"warning": null
191191
},
192+
{
193+
"auth": null,
194+
"description": "Username with password containing an unescaped colon",
195+
"hosts": null,
196+
"options": null,
197+
"uri": "mongodb://alice:foo:bar@127.0.0.1",
198+
"valid": false,
199+
"warning": null
200+
},
192201
{
193202
"auth": null,
194203
"description": "Username containing an unescaped at-sign",
@@ -207,6 +216,33 @@
207216
"valid": false,
208217
"warning": null
209218
},
219+
{
220+
"auth": null,
221+
"description": "Username containing an unescaped slash",
222+
"hosts": null,
223+
"options": null,
224+
"uri": "mongodb://alice/@localhost/db",
225+
"valid": false,
226+
"warning": null
227+
},
228+
{
229+
"auth": null,
230+
"description": "Username containing unescaped slash with password",
231+
"hosts": null,
232+
"options": null,
233+
"uri": "mongodb://alice/bob:foo@localhost/db",
234+
"valid": false,
235+
"warning": null
236+
},
237+
{
238+
"auth": null,
239+
"description": "Username with password containing an unescaped slash",
240+
"hosts": null,
241+
"options": null,
242+
"uri": "mongodb://alice:foo/bar@localhost/db",
243+
"valid": false,
244+
"warning": null
245+
},
210246
{
211247
"auth": null,
212248
"description": "Host with unescaped slash",

0 commit comments

Comments
 (0)