This video is available to students only

Pre-populate a WTForms Form And Prevent CSRF Attacks

Project Source Code

Get the project source code below, and follow along with the lesson material.

Download Project Source Code

To 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.

yumroad-app/yumroad/extensions.py
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:

yumroad-app/yumroad/extensions.py
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.

Unlock This Course

Get unlimited access to Fullstack Flask: Build a Complete SaaS App with Flask with a single-time purchase.

Thumbnail for the \newline course Fullstack Flask: Build a Complete SaaS App with Flask