Welcome to a quick tutorial on how to implement simple CSRF token protection in Python Flask. So you have heard of this “CSRF thing” to secure forms, but just how do we do it?
To implement CSRF token protection in simple terms:
- Generate a random token (string), and keep it in the user session.
- Place the token in a hidden form field.
- When the user submits the form, cross-check the submitted token against the session.
That’s all. But what the heck is CSRF? How does this “random string” prevent attacks? Read on!
TABLE OF CONTENTS
DOWNLOAD & NOTES
Here is the download link to the example code, so you don’t have to copy-paste everything.
EXAMPLE CODE DOWNLOAD
Just click on “download zip” or do a git clone. I have released it under the MIT license, so feel free to build on top of it or use it in your own project.
SORRY FOR THE ADS...
But someone has to pay the bills, and sponsors are paying for it. I insist on not turning Code Boxx into a "paid scripts" business, and I don't "block people with Adblock". Every little bit of support helps.
Buy Me A Coffee Code Boxx eBooks
WHAT IS CSRF?
Before we get into the code, here is a section to help absolute beginners answer the most important question – What in the world is CSRF?
CROSS-SITE REQUEST FORGERY
A type of malicious exploit of a website or web application where unauthorized commands are submitted from a user that the web application trusts.
– Wikipedia
In simple human terms:
- There are 2 websites. A “legit good website” and a “fake scam website”.
- The bad guys bait users into submitting a form on the “scam website” – That submits to the “good website” to do some funky things.
An example is worth a thousand words. Let us go through a simple CSRF attack example below.
SIMPLE CSRF ATTACK EXAMPLE
GOOD WEBSITE
<form method="post" action="http://good-site.com/delete">
<p>Enter "CONFIRM" to close your account.</p>
<input type="text" name="confirm" required>
<input type="submit" value="Go">
</form>
On the “good website”, we have a form to close the account – Just enter CONFIRM
and submit it to http://good-site.com/delete
. Of course, the user must be signed in.
BAD WEBSITE
<form method="post" action="http://good-site.com/delete">
<p>Enter your comment below.</p>
<input type="text" required>
<input type="hidden" name="confirm" value="CONFIRM">
<input type="submit" value="Go">
</form>
On the “bad website”, we have a fake comment form. If the user unknowingly submits this form and is signed in at the “good website”, the account will be closed.
PYTHON FLASK CSRF PROTECTION
As in the introduction, a simple form of CSRF protection is to put a “random token in the user session”. Here’s how.
QUICK SETUP
- Create a virtual environment
virtualenv venv
and activate it –venv\Scripts\activate
(Windows)venv/bin/activate
(Linux/Mac) - Install required libraries –
pip install flask pyjwt bcrypt
- For those who are new, the default Flask folders are –
static
Public files (JS/CSS/images/videos/audio)templates
HTML pages
PART 1) DUMMY HTML FORM
<form method="post" action="go" target="_blank">
<!-- (A) HIDDEN HTML TOKEN -->
<div>* This token should be hidden in your actual project</div>
<input type="text" name="token" value="{{ token }}">
<!-- (B) FIELDS AS USUAL -->
<input type="email" required name="email" value="jon@doe.com">
<input type="submit" value="GO">
</form>
First, let us start with the HTML form, this is your “regular form” with whatever fields you need. Just add a hidden field for the CSRF token – <input type="hidden" name="token" value="{{ token }}">
PART 2) FLASK CSRF
2A) INITIALIZE
# (A) INITIALIZE
# (A1) LOAD MODULES
from flask import Flask, request, session, render_template
import random, string
# (A2) FLASK INIT
app = Flask(__name__)
app.secret_key = "YOUR_SECRET_KEY"
# app.debug = True
# (A3) SETTINGS
HOST_NAME = "localhost"
HOST_PORT = 80
CSRF_CHAR = 12
Next, we will deal with the Flask server. This part should be pretty self-explanatory – Load the required modules, initialize, and define a few settings.
2B) HTML FORM & CSRF PROTECTION
# (B) ROUTES
# (B1) DUMMY HTML FORM
@app.route("/")
def index():
session["token"] = "".join(random.choices(string.ascii_uppercase, k=CSRF_CHAR))
return render_template("1-form.html", token=session["token"])
# (B2) SUBMIT FORM
@app.route("/go", methods=["POST"])
def go():
# (B2-1) TOKEN IS NOT "PRIMED"
if ("token" not in session or request.form.get("token") is None):
return "Token mismatch"
# (B2-2) CHECK SUBMITTED TOKEN VS SESSION
if (request.form.get("token") == session["token"]):
# OK - DO YOUR PROCESSING
# REMOVE TOKEN AFTER PROCESSING?
# session.pop("token")
return "It works!"
else:
return "Token mismatch"
# (C) START!
if __name__ == "__main__":
app.run(HOST_NAME, HOST_PORT)
This is where the “CSRF protection” happens:
- (B1) Before serving the HTML form, we generate a random string (CSRF token) and keep it in
session["token"]
. - (B1) Of course, we also pass the token into the HTML template, which will be placed in the hidden form field.
- (B2) When the user submits the form – We cross-check the submitted token against the session. Proceed only if they match.
SO… HOW DOES THIS STOP CSRF ATTACKS?
- The CSRF token is kept on the server side.
- Only the “good site” and the user’s HTML form will have a copy of the CSRF token.
- The “bad site” does not have the CSRF token, cannot be verified, and cannot proceed with the processing.
- If the “bad site” wants to perform a CSRF attack, they will have to get a hold of the CSRF token somehow.
Yes, a CSRF token acts as another layer of security but it is not foolproof. If the “bad site” somehow gets a hold of the token, attacks can still occur. See “more security” below.
EXTRAS
That’s all for the tutorial, and here is a small section on some extras and links that may be useful to you.
A REAL DANGER
Some people are going to laugh at “random CSRF string token”, think that it is a hassle and waste of time. But what if the CSRF attack:
- Changes the user’s password.
- Allows a third party to access the user’s account.
- Do fund transfers without the user’s knowledge.
- Buy something without the user’s permission.
Yep, you can see why CSRF and 2FA confirmation is such a big deal now.
MORE SECURITY
A quick disclaimer before the “experts” start to scream “not professional” – This is only a very simple example to help beginners grasp the idea of CSRF. A lot more can be done to secure the system:
- Enforce the use of HTTPS.
- Better encryption than just a random string…
- Add token expiry –
- Keep a timestamp in the session when the token is created.
- When the form is submitted, also check the timestamp. If it is longer than N minutes, the token is invalid.
- 2FA authentication. The user needs to clear a one-time password for secure transactions.
- Log the IP address and do geolocation checks. If the user suddenly “teleports” from one region to another – Something is fishy.
- Log the user agent and device – If the device is changed or not registered, something is fishy.
- Rate limiting – Transactions above a certain amount must pass 2FA, and cannot process more than a certain amount per day.
- Notifications – Notify the user whenever large/sensitive transactions are made.
The list can go on, you decide what needs to be implemented; If you are working on a million-dollar project, a “simple random CSRF token” is not enough.
LINKS & REFERENCES
- Cross-Site Request Forgery – Wikipedia
- Python Flask
THE END
Thank you for reading, and we have come to the end. I hope that it has helped you to better understand, and if you want to share anything with this guide, please feel free to comment below. Good luck and happy coding!