This is repository contains a full course project (1) for the Cyber Security Base course at the University of Helsinki.
The goal is to demonstrate 5 vulnerabilities from OWASP's Top Ten list (2021). The presence of these vulnerabilities is clearly documented in the code with their proposed fixes commented out of the final program.
This repository is a nice sample project for creating Capture The Flag (CTF) challenges.
Chat about all of those zero-days you found. What's the risk?
This project is a mockup of a for-hackers-by-hackers chat application do discuss the various Zero Day vulnerabilities they've discovered.
All chat-rooms are invite only, so you can easily keep your secrets to yourself...
Sign-up, create a profile for yourself and join chat-rooms or start a DM with another user.
The program has separate backend and frontend components. The frontend is made with Jinja2 templates using only HTML and CSS (trust me bro). The backend is a Python 3.13 native Flask webserver. It should work on Python 3.9 and later.
It should be noted that this is not a feature-complete chat application and some features were dropped during development due to time constraints. These features are missing at the time of writing this:
- Password reset
- Profile pages
- Admin management view (all chats & accounts listed etc.)
I understand that this stack is not for production or event best practice (if such a thing exists), but it is highly readable and simple. These aspects support the main goal of this project, but they might make contributing to this project undesirable. However, contributions are welcome!
There are 2 default accounts:
admin
username: admin
password: 1234
type: admin
testguy
username: testguy
password: 1234
type: user
This project demonstrates 5 different types of vulnerabilities.
All vulnerabilities are clearly commented in the code for clarity. Fixes are near the vulnerabilities in the code, but commented out.
Sometimes a fix simply involves the removal of some lines. In these cases instructions for which lines to remove for a fix are in the code as comments.
If you want to discover them for yourself, get the project setup, don't read the source code and skip the next section!
Reveal vulnerability list
A variant of the IDOR (Insecure Direct Object References, CWE-639) vulnerability is present in the message polling API.
The API does not verify that the user is in the chat new messages are being attempted to be retrieved from.
This allows any authenticated user to fetch all messages or poll for new messages from any chat.
Just add /?chat=<any id> as the path in the URL and you can see messages:
User foo can see the chat with id '2' (created by another user) even when they are not in any chat.
Sometimes the usernames show up as "unknown", if the user has not connected with the sender of the message via DM or group!
# Simply not checking the user is in the chat.Add the following in the api_poll_new_message method in api.py to check if the currently logged in user has access to the chat:
members = get_db().get_chat_members(chat_id)
if not list(filter(lambda member: member.user_id == session["user"]["id"], members)):
return "Unauthorized.", 401If you try to go to a chat you don't have access to, you'll see nothing and there's an error in the browser console:
All chat-rooms are invite only, except if you are admin ;) The developers accidentally created a SQL injection in the Invite API (CWE-89)
The /api/invite method handler is vulnerable to SQL injection via the tag parameter:
You can open the dialog from the top right when in a group.
Full payload: '; UPDATE Users SET is_admin = true WHERE tag = 'foo' --
This makes the user foo an admin on the whole service.
When you log in again, your session has updated to admin:
Not using parameters and allowing multiple queries by using executescript instead of execute:
def invite_user_to_group(self, chat_id: int, user_tag: str) -> bool:
self.connection.executescript(f"INSERT INTO ChatInvites (chat_id, user_id) SELECT '{chat_id}', id FROM Users WHERE tag = '{user_tag}'")
self.connection.commit()
return Truesrc/database/sqlite.py Line 154
Use parameters and plain execute. We need to replace the whole method.
def invite_user_to_group(self, chat_id: int, user_tag: str) -> bool:
user_to_invite = self.get_user(user_tag)
if not user_to_invite: return False
self.execute(f"INSERT INTO ChatInvites (chat_id, user_id) VALUES (?, ?)",
parameters=(chat_id, user_to_invite.id))
return Truesrc/database/sqlite.py Line 161
Now SQLI won't happen and we'll even get a clear error:
A mistake in the final stages of development lead to the Flask debug mode being left on. Whoops!
This leads to the attacker being able to see full stacktraces when an error occurs. If a user tries to login without typing in a password, it will cause an error in the Login API.
You can trigger the error using curl by leaving out the tag value from the expected request:
You can get the request_token and session cookie by simply loading the home page without logging in. The request_token is in the DOM in a hidden input field in various places and the session cookie in the cookie store (under Application tab in Chromium devtools)
Unfortunately the stacktrace happens to contain the hashed password of the user :/
Luckily any sane developer implements password strength requirements :)
app.debug = TrueJust remove the line and you'll now get a safe error:
You can gracefully patch the error too:
if "tag" not in request.form or "password" not in request.form:
return "Bad Request.", 400The Register API only requires the user to have some password. No strength check is present (CWE-521)
You'll be able to register an account even with a password with the length of 1:
# Simply not checking password strengthYou can check password strength with regex. Here we verify that:
- Minimum length of 8
- At least one uppercase letter
- At least one lowercase letter
- At least one special character
if not bool(match(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$", password)):
return redirect("/register?password-weak")You'll now get a clear error of a password that is too weak if you try to register with one:
Whoops! The developers forgot to use a secure and sufficiently random secret for generating session IDs (CWE-330)
This leads to an attacker being able to generate a session cookie for any account.
In the real world the attacker needs to brute-force the secret, but in our case, we can just copy it from the source code.
With the secret, we can edit the data stored in our session. In this case, let's make ourself admin by changing a 0 to a 1. First we'll decode our session data, edit it and then we'll re-encode it.
You can use a command-line tool called flask-unsign, which you can install with pip:
After setting our session cookie to the new cookie generated by flask-unsign we'll show up as admin:
This also means the backend will treat the current session as an admin session. As all permission checks are based on the values stored in the session and not live database data!
app.secret_key = "VERYSECURE"Generate a strong random secret when first running and save it to a file:
secret_key_file = Path("./.secret")
if not secret_key_file.exists():
secret_key_file.write_text(token_urlsafe(64), "utf-8")
app.secret_key = secret_key_file.read_text("utf-8")If you try your generated cookie (based on the bad secret), the web server will discard your cookie and give you a new one:
You can see I tried to set the cookie, but it resets, good!
You can run this project in code in Docker or from source. Setup and execution instructions for both methods below.
Personally, I'd recommend using Docker.
Running CTF challenges in Docker containers is the way to go.
Dockerfile and docker-compose.yaml files are provided.
Sit back and relax with:
docker compose upYou can skip this step if you want to handle the creation of a virtual environment manually. In that case, jump to step 1b.
This project used Python Dependency Manager (pdm) to handle dependencies and Python runtime requirements (eg. the virtual environment)
Before proceeding with installing pdm, make sure you have at least Python 3.9 installed in your system.
You can visit pdm-project.org for the most up to date install instructions or use any of these methods:
With pip (Any OS)
pip install --user pdmWith homebrew (MacOS)
brew install pdmWith Powershell (Windows)
powershell -ExecutionPolicy ByPass -c "irm https://pdm-project.org/install-pdm.py | py -"The installer will install PDM to the right place for your operating system:
- Unix:
$HOME/.local/bin - MacOS:
$HOME/Library/Python/<version>/bin - Windows:
%APPDATA%\Python\Scripts
Only do these steps if you skipped step 1! This approach is harder for inexperienced users and PDM is just really cool, so please use it :)
Make sure you have at least Python 3.9 available in your system. If not, please install Python >=3.13.
First create your virtual environment:
python3 -m venv venv
Then activate your virtual environment by executing the following based on your OS:
- Unix and MacOS:
source venv/bin/activate - Windows:
venv\Scripts\activate.bat
You can easily install all dependencies with pdm. If you are prompted to install a new version of Python, you can let it do that (handy right?)
pdm installWithout pdm
You can manually install dependencies via pip if you followed step 1b instead of 1a. Open pyproject.toml and install all dependencies from the dependencies list.
Install each dependency one-by-one with:
pip install package-name==versionReplace package-name with the name of the package and version with the version of the package.
The frontend and API will be available in port 5000.
You can run the project with pdm:
pdm run startWithout pdm:
When the virtual environment and dependencies have been manually installed:
python3 src/main.pyLicensed under the MIT license. See /License file for specifics.












