Exercise 20: Bypassing Web Authentication

This exercise uses a simple web application written in Python using the Flask microframework. The second half of the exercise involves fixing the vulnerability in the application, and you will find this part easier if you already have some experience with Python and Flask.

If you have no such experience, there are plenty of quick tutorials out there. Flask’s own Quickstart introduction gives you most of what you need to know. If you don’t want to work through that, team up with someone who has used Flask and do the second part of the exercise together.

Exploring The Vulnerability

  1. Download authbypass.zip and unzip it into a new directory created for this exercise. This will give you a vulnerable web application, in the file app.py. Open a terminal window and cd to the directory containing this file.

    To run the app, you will need to use Python 3, in an environment where Flask is available. You can get this on SoC Linux machines by activating the Anaconda3 Python distribution, like so:

    module load legacy-eng
    module add anaconda3/2020.11
    

    After you’ve done this, enter the following commands:

    export FLASK_APP=app.py
    export FLASK_DEBUG=1
    flask run
    

    As noted in Exercise 17, these commands assume the use of a Linux, macOS or WSL command line environment. Adjust them as described in that exercise if you are using the standard Windows cmd shell or Powershell.

  2. With the vulnerable application running, visit http://localhost:5000 in a web browser. You should see a login page. Enter user for the username and anything other than user for the password and click Log In. You should see the login form redisplayed with an error above it.

    Try again, this time entering user for both username and password. This should take you to the user’s home page.

    Go to the browser address bar and reenter http://localhost:5000. This will forward you to the login page again. Now enter admin for both username and password, then click Log In. This should take you to home page for the administrator.

    At first glance, this application seems to be authenticating users correctly, but all is not what it seems…

  3. Back in the terminal window, Press Ctrl+C to kill the server, then enter flask run to restart it.

    Enter http://localhost:5000 in the browser address bar again. After you’ve been redirected to the login page, change the URL in the address bar to http://localhost:5000/user and press Enter. Notice how you are taken to the user’s home page without having to supply their username and password!

    Repeat, this time editing the URL to http://localhost:5000/admin. Again, you are able avoid entering a username and password. This is even more serious because the admin page has elevated privileges – simulated here by the ‘Do Dangerous Stuff’ button.

  4. It’s time to find out what is going on. Kill the server with Ctrl+C, then open app.py in a text editor and examine the application’s code.

    You can see here that the home() function handles visits to the root URL of the application by redirecting to a login() function. The login() function presents the login form to the user and also handles form input, using the latter to decide where the user should be redirected.

    Now look at the user() and admin() functions. Here’s the code for the latter:

    @app.route("/admin")
    def admin():
        return render_template("admin.html")
    

    This defines /admin as a path that triggers the admin() function, and this simply returns the administrator page to the browser without checking that the visitor has actually logged in as the administrator.

Fixing The Vulnerability

The second part of this exercise will implement a simple fix to the vulnerability seen earlier. The approach used here illustrates the principles but should not be regarded as an especially good solution!

  1. The user() and admin() functions need to know whether the initiator of the request has been authenticated, and who the initiator is; only then can they decide whether to authorize the request or not. Information therefore needs to be passed from the login() function to the user() and admin() functions. We can use sessions to do this.

    Start by editing app.py and adding an import statement for Flask’s session object:

    from flask import session
    

    You also need to give the app a secret key that it can use to sign session cookies, so add a line of code that initialises app.secret_key to a random string of characters.

  2. Modify login() so that it stores the username in the session object. This object behaves like a Python dictionary, so you can do this with

    session["username"] = username
    

    Obviously, this should be done only if the user has authenticated successfully.

  3. Modify user() and admin() so that they use the session object to check the identity of the logged-in user before returning the relevant web page. Remember that username might not be in the session dictionary if the user hasn’t authenticated!

    If the correct value for username is not present, both functions should issue a 401 Unauthorized HTTP error. The easiest way of doing this in Flask is to call the abort function like so:

    abort(401)
    

    Note that you’ll need to import this function from the flask module, just as you did with the session object.

    Image for 401 errors, from the HTTP Cats website

    https://http.cat

  4. Optional: skip this step if you want…

    Implement a custom handler for 401 errors. Add the following to the bottom of app.py:

    @app.errorhandler(401)
    def unauthorized(error):
        return render_template("unauthorized.html"), 401
    

    Then create a suitable template named unauthorized.html in the templates directory. Use the other templates in that directory to guide you. For a consistent style, try representing the error message as a Bootstrap 5 alert.

  5. It’s important that users can log out, and that the session object no longer holds their username once they’ve done so. Implement this by writing a logout() function:

    @app.route("/logout")
    def logout():
        # (1) Remove username from session
        # (2) Redirect to login page
    

    Add the code suggested by the comments above, then add ‘Log Out’ buttons to the user and admin templates. The following HTML will do the trick:

    <a href="/logout" class="btn btn-primary">Log Out</a>
    
  6. Use flask run to run the application again. Make sure that it works as expected and that bypassing authentication is no longer possible.

    NOTE: This demonstrates a solution but isn’t the best way of fixing the problem! Any web framework worth its salt will provide you with better and more secure options for managing users and logins. For example, Flask provides the Flask-Login extension, with which you can indicate that login is required by adding the login_required decorator to your request handlers:

    from flask_login import current_user, login_required
    ...
    @app.route("/admin")
    @login_required
    def admin():
        if current_user.get_id() == "admin":
            return render_template("admin.html")
        else:
            abort(401)
    

    A request made for /admin from an unauthenticated visitor results in a redirect to a login handler function (not shown), after which the visitor will be sent back here.