Pre-populate a WTForms Form And Prevent CSRF Attacks
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
Editing Data with Forms
You can use WTForms to prepopulate data for forms as well, which is handy when you are editing records. To prepoulate data in a form from a object, WTForms provides a method called process
where we can pass in raw form data and/or an object
@products.route('/<product_id>/edit', methods=['GET', 'POST'])
def edit(product_id):
product = Product.query.get_or_404(product_id)
form = ProductForm()
if form.validate_on_submit():
product.name = form.name.data
product.description = form.description.data
db.session.add(product)
db.session.commit()
elif not form.errors:
# We do this to make sure we're not overwriting a user's input
# if there was an error.
form.process(formdata=form.data, obj=product)
return render_template('products/edit.html', form=form, product=product)
A shortcut for the above logic, is to simply pass in the object when instantiating the form.
@products.route('/<product_id>/edit', methods=['GET', 'POST'])
def edit(product_id):
product = Product.query.get_or_404(product_id)
form = ProductForm(product)
if form.validate_on_submit():
product.name = form.name.data
product.description = form.description.data
db.session.add(product)
db.session.commit()
return render_template('products/edit.html', form=form, product=product)
The template for the edit form (yumroad/templates/products/edit.html
), looks very similar to the creation form, but uses the edit
route instead. It's useful to have a separate template since we may want to customize what fields we allow on the edit field.
{% block title %} Edit {{ product.name }} {% endblock %}
{% block content %}
<div class="container">
<form method="POST" action="{{ url_for('products.edit', product_id=product.id) }}">
{{ render_field(form.name) }}
{{ render_field(form.description) }}
<button type="submit" class="btn btn-primary">Edit</button>
</form>
</div>
{% endblock %}
CSRF
Our application right now will accept any POST requests and process the change if the user is logged in (by looking at the cookies). This creates a security vulnerability called Cross Site Request Forgery where other websites can create requests on behalf of users who happened to be logged into our site and land on a malicious webpage. It would be bad if we allowed malicious websites the opportunity create products, or worse transfer funds, on behalf of our users.
You can read more about CSRF here.
Setting up CSRF Protection
Flask WTForms allows us to enable CSRF Protection. To support it, we'll need to
In extensions.py
, we can import CSRFProtect
from flask_wtf.csrf
and instantiate it.
extensions.py
should look like this.
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
db = SQLAlchemy()
csrf = CSRFProtect()
In yumroad/__init__.py
, we will have to call csrf.init_app
with our app
to set it up.
yumroad/__init__.py
should look like this:
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
db = SQLAlchemy()
csrf = CSRFProtect()
You will likely also need to set SECRET_KEY
in order to generate CSRF tokens. We will discuss how the SECRET_KEY
is used in the next chapter.
To quickly generate a random value using Python, you can use the os.urandom
method. Run this command in your terminal to get a random string of bytes: python -c 'import os; print(os.urandom(16))'
For the development environment, we can add a simple default value. In yumroad/config.py
, ensure that the DevConfig
specifies a SECRET_KEY
.
class BaseConfig:
TESTING = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = os.getenv('YUMROAD_SECRET_KEY', '00000abcdef')
...
class ProdConfig(BaseConfig):
SECRET_KEY = os.getenv('YUMROAD_SECRET_KEY')
Implementing CSRF Protection in Forms
Implementing CSRF protection is straight form thanks to flask_wtf
. All forms have a csrf_token
field that we need to render in the template. The field is configured to be hidden but the value of the csrf_token
field will be checked through the form validations to ensure that it the provided token is valid.
This lesson preview is part of the Fullstack Flask: Build a Complete SaaS App with Flask course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Fullstack Flask: Build a Complete SaaS App with Flask with a single-time purchase.

[00:00 - 00:09] In the last video, we went over how to use WTFarms to create records. In this video, we're going to go over how to use WTFarms to edit records.
[00:10 - 00:14] We're also going to enable Cross-Site Request Forgery Protection. So let's get started.
[00:15 - 00:22] In our code, we have a route to create records. Similarly, we're going to want a route to edit a particular record.
[00:23 - 00:32] So what I'm going to do is I'm going to copy this route, but we're going to make a few changes. The first one is that this is going to have to operate on a specific product ID .
[00:33 - 00:44] So we're going to call it edit there. Then once it's operating on a specific product ID, we're going to want to get the product that it's associated with.
[00:45 - 00:57] When we're here and we have the data, we're going to want to instead just edit the product that we fetched. So product.name is going to be equal to form.name.data and product.
[00:58 - 01:08] Description is going to be equal to form.description.data. Now that we have this, we can take a look at whether we need a separate form here.
[01:09 - 01:19] So looking at forms.py, product form has the field name and description. We could edit this and create a edit product form field.
[01:20 - 01:26] If say we didn't want our users to be able to edit the name of product. But in our case, I think it's fine if they add anything.
[01:27 - 01:40] So we can actually just go ahead and use the same exact form class. Okay, so coming back here, we want our form to be pre-filled with our object that we've loaded from our database.
[01:41 - 01:50] So we can actually do that by just passing in the object to the form. So if we say object is equal to product here.
[01:51 - 02:03] wt forms will take care of pre-filling the objects because between the form and the model, the fields match exactly. Great.
[02:04 - 02:09] So this will pre-fill our form and this will set the right data. And we still want this redirect.
[02:10 - 02:14] I think that makes sense. So here on the edit, we're going to create the edit.html form.
[02:15 - 02:20] We're also going to pass in the product in case we need to reference it. Any of the details on that page.
[02:21 - 02:27] Okay, here's the create form. We're going to take that and use that for edit page as well.
[02:28 - 02:44] So we're going to say edit and then here we can actually just render the name of the product. Okay, so form field.name submit and instead of edit product, let's try that.
[02:45 - 02:48] Go back here. We're going to have to pass in the product ID in order to make this all work.
[02:49 - 02:53] All right. Now let's check it out and see if it worked.
[02:54 - 03:12] So here I can see the edit page and it loads all the data here. And so I can change this to see my technical book and that works and then I can go back and edit and I can try and do something invalid like changing the name to a SQL character and then it complains.
[03:13 - 03:20] And if you go back, we verify that it didn't actually change the record, which is great. Okay.
[03:21 - 03:27] Now I'm going to talk about cross site requests for your protection. This is important security feature to have.
[03:28 - 03:38] You can read more about it on this website. Here I have our form and I can craft the HTML form that looks innocuous.
[03:39 - 03:45] You know, who knows what it does, but it looks innocuous enough. And then as soon as I enter it, it actually went in and changed our data.
[03:46 - 03:56] So now if I look at this, that looks pretty bad. So we're going to have to figure out a way to solve that error and the way to do that is by using cross site request for your protection.
[03:57 - 04:13] Fortunately, last W T forms comes with a built in way to handle that in our application. So here what we're going to do is we're going to say from last W T forms dot CS RF import CSRF protect.
[04:14 - 04:23] And we're going to then initialize that. So we can say that that CSRF is equal to CSRF protect going an application in it.
[04:24 - 04:47] We're then going to want to import that from our application and then in it app . Now what this does by default is it makes that every request that's getting a post or a put or something that's changing data like a delete request as well is also going to require a CSRF token to be attached to it.
[04:48 - 05:01] And in order for CSRF token to be attached to it, we're going to have to implement a secret key field as well within our configuration. So let's go ahead and edit our config to make sure that we're enabling that to be true.
[05:02 - 05:16] Okay, so first off, we had earlier disabled CSRF protection. So we'll want to disable in test mode for convenience sake, but in production and in dev mode, we'll want it to be enabled.
[05:17 - 05:32] So let's go ahead and actually set that to true there. And we're also going to need to set a secret key and a secret key is something that flas uses to sign certain fields so that it knows that they weren't tam pered with.
[05:33 - 05:53] In order to generate a value that we can use for the secret key, what we can do is go into our terminal and run a quick Python script to generate something that makes sense. So in our terminal, what we can do is we can just launch Python and run something like this import UUID.
[05:54 - 06:11] And then we can do UUID for the hex. Okay, so this value is something that we can use as a secret key for now.
[06:12 - 06:35] In our dev environment, it really doesn't quite matter what our secret key is, but in production, we won't want to hard code that into our application. So what we can do is we can set our secret key is equal to OS dot get end, which retrieves an environment variable we can call this one secret key.
[06:36 - 06:48] And if it's not set, we're going to let it be done and let our application complain if it's required. But by default in our dev config, we're going to go ahead and set some value here.
[06:49 - 06:59] We can even get an environment variable from this function. Okay, so now we have a secret key set.
[07:00 - 07:13] At this point, we'll be able to test if our W two forms integration works. Okay, back in Firefox, I loaded up the edit page and you can see that the bad data is here.
[07:14 - 07:25] So what we can do is we can try and change this to be good data, for example, and then hit edit product. And then it's going to issue this requesting the CSRF token is not there.
[07:26 - 07:37] And that's because we're going to have to include the CSRF token with every request we make. And flask W two forms is going to check to make sure the CSRF token we included is valid.
[07:38 - 07:54] And once we do that, that means that the vulnerability here that allowed us to set different values will no longer work. Okay, so to do this, we're going to go into VS code and we're going to add a simple field to our forms.
[07:55 - 08:07] So on the edit form, in addition to doing form.name, we're also going to want to render the CSRF token. And by doing that, that will introduce the token that will send.
[08:08 - 08:15] Here's what the field looks like. The CSRF token field that we just rendered in our form automatically gets rendered as a hidden field.
[08:16 - 08:26] So it doesn't show up in our user input, but it shows up inside of the form, which means that when we submit it, it's sent alongside the rest of our data. To that point.
[08:27 - 08:50] Now, going back to our browser, we can see if we try and change this to be good data, that will work. But then going back in our vulnerability here and doing this won't work because this other site will not know what the CSRF token is.
[08:51 - 09:00] Great. Another thing you might need to be able to do is show your CSRF token in your own JavaScript.
[09:01 - 09:07] If you have something custom you're doing there. And the way you can do that is by using a function that.
[09:08 - 09:20] Plus, wt forms provides for you and by setting a property on your HTML document . So here I'm going to add a meta tag for CSRF token and I can set the content is equal to.
[09:21 - 09:27] And then there's a function that we can call that's introduced into ginger. That's called CSRF token.
[09:28 - 09:40] And the CSRF token is going to be preset. And now our JavaScript, if we need it, if it's sending an Ajax request or something, it can refer to the CSRF token here.
[09:41 - 09:56] Looking back at our code, we can now see that in the head, the CSRF token is set there for us. Alright, now let's talk about testing this.
[09:57 - 10:15] We're going to want to make sure that again that in our config, we've turned the ability to have CSRF off. The reason we'll want to do that is because adding CSRF request in all of our tests for all of the forms we do, will make things pretty difficult.
[10:16 - 10:27] Going back to test product, we can expand on test product to test some of the added forms as well. So simple test we can add is one that the new page load works.
[10:28 - 10:37] So all this does is it issues a get request and it makes sure that that works correctly. Another test we can add is something to test that the page creation works.
[10:38 - 10:59] And so what we do here is we make a post request with some values and then we ensure that the data is properly updated. We can also similarly test that our data from invalid tests also get invalid ated and it must show us that error message.
[11:00 - 11:09] The next thing we can test is our edit page. So we'll want to make sure that our edit page actually shows up when we issue a get request to it.
[11:10 - 11:21] And so this ensures that our edit page shows up and includes the contents of the product in there. Next, we'll want to test the edit submission page.
[11:22 - 11:40] To test that the edit form actually works, we're going to do something similar where we're going to check that the old description actually changes once we submit the form. The last step is testing that an invalid edit also fails.
[11:41 - 11:52] So once we have this, we can go back and contact.py and check that everything is to order. So one change I've made here is I've made sure that secret key is always defined at some level.
[11:53 - 12:12] So we can make this one simply a random number and then in our production config, we'll want to make sure that the secret key is actually set. The reason we'll need this is because you need a secret key to set a lot of values and to create certain things like logins, which we'll talk about in the next chapter.
[12:13 - 12:22] So this just sets us up for that as well. Okay, so I've moved the secret key into our prod config here.
[12:23 - 12:31] Now let's go ahead and run our test suite and see what happens. Great.
[12:32 - 12:44] And then if we run it with some code coverage metrics, we can ensure that we're still at 100% code coverage, which is great. The next thing we're going to work on is user login and authentication.