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.
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.
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…
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.
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.
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!
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.
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.
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.
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.
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>
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.
□