Python, like most modern programming languages, handles runtime errors with exceptions. For every Python programmer, the try ... except block is basic equipment. Exceptions are used for runtime errors and operating-system errors, and they are also closely connected with Python’s dynamic typing, as shown by an example of exotic uses of exceptions. This post looks at code that seems perfectly fine but contains a serious bug. The idea came from an article on Zdroják.cz.

The article discussed authentication and authorisation in the web.py framework. Petr Horacek described his own module. The example code also contained a bug in exception handling during login; see my comment. The bug concerns Python 2.x. In Python 3, the notation that leads to it is a syntax error.

The bug is not obvious at first glance. The code works, everything is syntactically valid, and yet something is wrong. First, a module defining custom exceptions as classes, say mod_exc.py:

class UserNotFound(Exception):
    pass

class WrongPassword(Exception):
    pass

Now the code. Imagine POST() as a handler for an HTTP POST request with user and passwd parameters. These values are verified in login() against PWD_DICT. If everything is fine, login() succeeds; otherwise it raises UserNotFound or WrongPassword.

import mod_exc
import sys

PWD_DICT = {'honzas': 'hPsWd'}

def login(user, passwd):
    try:
        if PWD_DICT[user] != passwd:
            print 'wrong password'
            raise mod_exc.WrongPassword
    except KeyError:
        print 'user not found'
        raise mod_exc.UserNotFound

def POST(**args):
    usr = args.get('user')
    passwd = args.get('passwd')
    try:
        login(usr, passwd)
        print 'access granted'
    except mod_exc.UserNotFound, mod_exc.WrongPassword:
        print 'access denied'
    print

POST(user='honzas', passwd='hPsWd')
POST(user='test', passwd='hPsWd')
POST(user='honzas', passwd='foobar')
POST(user='test', passwd='hPsWd')
POST(user='honzas', passwd='foobar')

The output is as expected:

access granted

user not found
access denied

wrong password
access denied

user not found
access denied

wrong password
access denied

The code still contains a fundamental, and in this context potentially security-relevant, bug. The first indicator appears when we change the sequence of POST() calls:

POST(user='honzas', passwd='hPsWd')
POST(user='honzas', passwd='foobar')
POST(user='test', passwd='hPsWd')

The output becomes a traceback:

access granted

wrong password
Traceback (most recent call last):
  File "exceptions.py", line 28, in
    POST(user='honzas', passwd='foobar')
  File "exceptions.py", line 20, in POST
    login(usr, passwd)
  File "exceptions.py", line 10, in login
    raise mod_exc.WrongPassword
mod_exc.WrongPassword

Tracking the Bug

Add debug prints to POST():

def POST(**args):
    usr = args.get('user')
    passwd = args.get('passwd')
    try:
        login(usr, passwd)
        print 'access granted'
    except mod_exc.UserNotFound, mod_exc.WrongPassword:
        print 'access denied'
        type, value, traceback = sys.exc_info()
        if type == mod_exc.UserNotFound:
            print '... user not found'
        else:
            print '... wrong password'
    print

The output is surprising:

access granted

user not found
access denied
... user not found

wrong password
access denied
... user not found

In every case the caught exception appears to be UserNotFound, even when the username is correct and the password is wrong.

Where Did the Bug Probably Happen?

The bug was created by the historical design of Python 2.x. Python 3 prevents it at syntax-check time. The cause is missing parentheses in the except branch, where the code was meant to catch exceptions of two classes:

except mod_exc.UserNotFound, mod_exc.WrongPassword:

In Python 2 this is equivalent to catching UserNotFound and binding the caught exception under the name mod_exc.WrongPassword:

except mod_exc.UserNotFound as mod_exc.WrongPassword:

Instead of catching either exception class, it always catches only UserNotFound and stores the caught exception into the mod_exc module under the name WrongPassword. After that, raising mod_exc.WrongPassword actually raises UserNotFound again.

The Fix

The fix is simple: use parentheses around the tuple of exception classes. With the corrected POST() function, the output is:

access granted

user not found
access denied
... user not found

wrong password
access denied
... wrong password

Conclusion

Although the problem affects only Python 2.x, its consequences are serious because Python 2 was widespread and actively supported at the time. In a broader sense, this resembles overwriting part of the program through a carefully chosen sequence of inputs, which can lead to security bugs.

Notice that the bug is mainly enabled by defining exceptions in a separate module, importing that module, and referring to exceptions with dotted notation. That makes it possible to overwrite one name permanently with another object and change the behaviour of code that runs later.

One final kick at Python 2.x: it is good that this notation is no longer supported in Python 3. It is historical baggage from Python’s rather chaotic early development.