0

I've been pulling my hair out on this error message. Please bear with me as I am just not that smart. I have a flask application being made in python with CSRFProtect enabled across the application.

Web Service in python

'''Using flask to create the python ws'''
from flask import Flask, request, session
from flask_cors import CORS
from flask_wtf.csrf import CSRFProtect
from dotenv import dotenv_values
from Validator import geia_std_xsd_validation

# returns a dict of the values contained within the .env file
config = dotenv_values('.env')


ALLOWED_EXTENSIONS = {'xml'}

APP = Flask(__name__)
SECRET_KEY = 'test12'
csrf = CSRFProtect()
origin_array = config['WHITELIST_ARRAY']
cors = CORS(APP, origins=origin_array, expose_headers=['Content-Type', 'X-CSRFToken'], supports_credentials=True)
csrf.init_app(APP)
APP.config['SECRET_KEY'] = SECRET_KEY


APP.vars = {}

def allowed_file(filename):
    """
    Checks to make sure the uploaded file is in the list of allowed extensions.
    """
    return '.' in filename and \
        filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

# Do we need this?
@APP.route('/', methods = ['GET'])
def server_up():
    '''This method handles the calls to our ws'''
    if request.method == 'GET':
        return {
            "message": APP.config.get('WTF_CSRF_FIELD_NAME'),
            'sesh': session
        }

@APP.before_request
def test():
    session['csrf_token'] = 'test12'
    print('before')

@APP.route('/xsd_validator', methods = ['POST'])
# @csrf.exempt # we'll figure this out later...
def xsd_validation():
    '''This method handles the calls to our ws'''
    if request.method == 'POST':
        print('HITTING')
        # If there isn't a file in the request.
        if len(request.files) <= 0:
            print('No files in response')
            return 'No file sent'

        # # Set the file from the request.
        file = request.files['testFile']

        # # Make sure we have a filename so we can check extension.
        if file.filename == '':
            print('Blank filename')
            return 'Invalid filename'

        # # If there is a file and we allow that file type then validate it against the schema.
        if file and allowed_file(file.filename):
            print('Got into the file statement')
            file_text = file.read()
            errors = geia_std_xsd_validation(file_text)
            return errors

    return 'Nothing Happened'


if __name__ == "__main__":
 // APP.run(//the sauce)

I have another web service made in expressjs that will hit this web service with a file for xml validation with this function

const testTHATSHIZZLE = async (req, res) => {
  const csrf = 'test12'
  const file = req.files.testFile
  const name = file.name

  // not recognizing blob D:
  const submittedFile = new FormData()
  const arrayBuffer = Uint8Array.from(file.data).buffer
  const nodeBuffer = Buffer.from(arrayBuffer)
  // const fileBlob = new Blob([file.data])
  submittedFile.append('testFile', nodeBuffer, name)
  await axios.post('https://localhost:6030/xsd_validator', submittedFile, {
    csrf_token: csrf,
    withCredentials: true,
    headers: {
      'X-CSRFToken': csrf,
      'Content-Type': 'multipart/form-data'
    }
  }).then(response => {
    res.send(response.data)
  }).catch(error => {
    console.log(error)
    res.send(error)
  })
}

Just for baseline, this totally works when csrf.exempt is uncommented out above the route I need this to take. But when csrf is enabled for the route, I am getting the bad request 400 error CSRF session token missing. I am looking all over and I just can't figure this out. When I put the flask app in debugger and try to see what session variables there are it is an empty ImmutableDict. As you can see I am setting the csrf tester token under the header, adding withCredentials to the axios call. The flask app has cors enabled and supports credentials.

I am so at a loss as to how axios and my flask app are interacting. I am able to set session variables in the before_request method for the GET route, but it fails before I can hit the POST route. Not that I thought I SHOULD be doing that. I just wanted to test.

There's no other variables in session?

I don't know if I'm invoking this wrong or not, but I am hoping someone knows more about hitting a flask app with csrf protections from an external web service not using flask forms.

I've tried going through the gitlab repo for flask-wtf and looking right at csrf.py, but that just tells me this should be working already. I don't know why a session isn't established when I make an axios call.

Thome
  • 31
  • 7
  • you can try to add csrf_token: csrf in the request body of axios because it is a post method submittedFile.append('csrf_token', csrf ) but i dont know how do you get this token const csrf = 'test12' ????? if you have problem with that also you need to create an endpoint to request an csrf_token after that you can send your form – Christa Aug 27 '23 at 03:10
  • I tried both solutions, but I am still ending up with 'The CSRF session token is missing.' And the csrf = 'test12' is just something I hard-coded in for testing. – Thome Aug 28 '23 at 16:34
  • **u need to request csrf from the server. enable CSRF protection** : from flask_wtf import CSRFProtect; crsf = CSRFProtect(app); **create api to get csrf** from flask_wtf.csrf import generate_csrf @app.route("/csrf") def get_csrf(): response = jsonify(detail="success") response.headers.set("X-CSRFToken", generate_csrf()) return response **now before the axios post you need to get a token from server** axios.get('https://localhost:6030/csrf').then(() => {axios.post('https://localhost:6030/xsd_validator')... }) **and remove** 'X-CSRFToken': csrf, **from the post header and also body** – Christa Aug 28 '23 at 17:57

2 Answers2

1

a new crsf token should be requested by client from server each time you need to send form via axios (xmlhttprequest) and you cant hard coded it because crsf token is calculated based on the secret key (WTF_CSRF_SECRET_KEY or SECRET_KEY if WTF_CSRF_SECRET_KEY not provided).

app.config['SECRET_KEY'] = "Your_secret_string"

app.config['WTF_CSRF_SECRET_KEY'] = "Your_wfk_csrf_secret_string"

your config :

# your settings.py
SESSION_PROTECTION = "strong"

# help prevent XSS. 
SESSION_COOKIE_HTTPONLY = True

# you will want to set this to False during development.
# this is due to the lack of SSL during development.
SESSION_COOKIE_SECURE = True

You'll also want to enable CSRF protection. This is done using the flask-wtf package.

from flask_wtf import CSRFProtect

crsf = CSRFProtect(app) 
# or crsf = CSRFProtect()
# csrf.init_app(app)
# if you're using factory config.

example endpoint to get csrf token.

from flask_wtf.csrf import generate_csrf

@app.route("/csrf")
def get_csrf():
    response = jsonify(detail="success")
    response.headers.set("X-CSRFToken", generate_csrf())
    return response

now on your client app e.g React, Vue, etc. Using axios for instance, you'll add this to your index.js to make sure every request made to that domain includes all the cookies available to the specific domain.

axios.defaults.withCredentials = true

or for each request :

withCredentials: true,

or by block of code :

const instance = axios.create({
  withCredentials: true
})

finally the client :

const testTHATSHIZZLE = async (req, res) => {

  const instance = axios.create({
      withCredentials: true
  })

  const file = req.files.testFile
  const name = file.name

  // not recognizing blob D:
  const submittedFile = new FormData()
  const arrayBuffer = Uint8Array.from(file.data).buffer
  const nodeBuffer = Buffer.from(arrayBuffer)
  // const fileBlob = new Blob([file.data])
  submittedFile.append('testFile', nodeBuffer, name)
  // we get new crsf token (instance variable refer to axios withCredentials: true)
  const response  = await instance.get('https://localhost:6030/csrf' /*, { withCredentials: true }*/)
  // we do request
  await instance.post('https://localhost:6030/xsd_validator', submittedFile, {
    /*withCredentials: true,*/
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  }).then(response => {
    res.send(response.data)
  }).catch(error => {
    console.log(error)
    res.send(error)
  })
}

if you want to only test the app with hard coded token you can try this method (i dont test it) print a crsf token from your app :

// add this line to the main file
print(generate_csrf())
// should output a token based on the secret key
xoxoxoxo

copy this token and use it as hard-coded token :

//paste the token in server side
session['csrf_token'] = 'xoxoxoxo'

//paste the token to axios
const csrf = 'xoxoxoxo'
Christa
  • 398
  • 7
  • I bless you and your response. Because of how you structured the get endpoint and the response I was able to see that my session data was not persisting between axios calls for whatever reason. I had wrapped my app in cors AND made sure I was calling the 'withCredentials' attribute set to true. I made two GET endpoints and returned the session.sid from them. Then when I went to my browser and navigated to my flask app's localhost port I was able to see the same session sid. However each axios call had a different sid no matter what I did. I'll make another answer with full details. – Thome Aug 31 '23 at 02:19
  • @Thome first remove **session['csrf_token'] = 'test12'** from this block **@APP.before_request** because Flask sends the session cookie to the client only when you create a new session or modify an existing session refer to: https://overiq.com/flask-101/sessions-in-flask/ . to force Flask session to persist changes add **session.modified = True;** (refer : https://stackoverflow.com/questions/39261260 ) and wrap you app with Flask-CORS and add **@cross_origin(supports_credentials=True)** to routes or **CORS(app, support_credentials=True)** refer https://stackoverflow.com/questions/52733540 – Christa Aug 31 '23 at 05:43
0

So following the advice from @Christa I set up the server side like so: Flask Application xsd_validation.py

  '''Using flask to create the python ws'''
from flask import Flask, request, jsonify
from flask_cors import CORS
from flask_wtf import CSRFProtect
from flask_wtf.csrf import generate_csrf
from dotenv import dotenv_values
from Validator import geia_std_xsd_validation

# returns a dict of the values contained within the .env file
config = dotenv_values('.env')
origin_array = config['WHITELIST_ARRAY']


ALLOWED_EXTENSIONS = {'xml'}

app = Flask(__name__)
# app config
SECRET_KEY = 'cool_secret'
app.config['SECRET_KEY'] = SECRET_KEY
app.config['SESSION_PROTECTION'] = 'strong'
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE'] = True
app.config['WTF_CSRF_SSL_STRICT'] = False # going to figure out the ssl stuff later
app.vars = {}

# enabling csrf on the app
csrf = CSRFProtect(app)

CORS(app, origins=origin_array, supports_credentials=True)

def allowed_file(filename):
    """
    Checks to make sure the uploaded file is in the list of allowed extensions.
    """
    return '.' in filename and \
        filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

# Do we need this?
@app.route('/', methods = ['GET'])
def server_up():
    '''This method handles the calls to our ws'''
    if request.method == 'GET':
        return {'message': 'The container/app/server is up'}

@app.route('/csrf', methods = ['GET'])
def get_csrf():
    response = jsonify(detail='success')
    response.headers.set('X-CSRFToken', generate_csrf())
    return response

@app.route('/xsd_validator', methods = ['POST'])
def xsd_validation():
    '''This method handles the calls to our ws'''
    if request.method == 'POST':
        print('HITTING')
        # If there isn't a file in the request.
        if len(request.files) <= 0:
            print('No files in response')
            return 'No file sent'

        # # Set the file from the request.
        file = request.files['testFile']

        # # Make sure we have a filename so we can check extension.
        if file.filename == '':
            print('Blank filename')
            return 'Invalid filename'

        # # If there is a file and we allow that file type then validate it against the schema.
        if file and allowed_file(file.filename):
            print('Got into the file statement')
            file_text = file.read()
            errors = geia_std_xsd_validation(file_text)
            return errors

    return 'Nothing Happened'


if __name__ == "__main__":
  app.run(host='0.0.0.0', port='6030', ssl_context=('./ssl/172-21-128-15.pem', './ssl/172-21-128-15.pem'), debug=True)

And this was perfectly what was needed server side! You'll see that I am making the setting WTF_CSRF_SSL_STRICT = False And without that I was then getting a referrer error. But at least this way I can reach the endpoint. Something to figure out later. The core of the problem was the missing session token.

With the way Christa had me build my response I was able to see that a new session was established on every axios call no matter what. I had made an instance that had withCredentials enabled. My python app was wrapped in cors that supported credentials. It just refused to persist a session when using axios. So I had to change my client side to utilize an axios instance wrapped with tough-cookie. I got this solution persist axios call between sessions and then implemented that on my client side.

const axios = require('axios').default
const { CookieJar } = require('tough-cookie')
const { wrapper } = require('axios-cookiejar-support')

module.exports = function () {
  const jar = new CookieJar()

  const client = wrapper(axios.create({ jar }))

  return client
}

This I called into my client page like so

const client = require('../client/persistent-client')

const persistentAxios = client()

and then used that when making my calls to the server protected by csrf

const getErrors = async (file, name) => {
  // not recognizing blob D:
  const submittedFile = new FormData()
  const arrayBuffer = Uint8Array.from(file.data).buffer
  const nodeBuffer = Buffer.from(arrayBuffer)
  // const fileBlob = new Blob([file.data])
  // which version to push to container?
  // submittedFile.append('testFile', fileBlob, name)
  submittedFile.append('testFile', nodeBuffer, name)
  // process.env.VALIDATOR_WS_URL
  const response = await persistentAxios.get('https://localhost:6030' + '/csrf', { withCredentials: true })

  return await persistentAxios.post('https://localhost:6030' + '/xsd_validator', submittedFile, {
    withCredentials: true,
    headers: {
      'X-CSRFToken': response.headers['x-csrftoken'],
      'Content-Type': 'multipart/form-data'
    }
  }).then(response => {
    console.log(response.data)
    return response.data
  }).catch(error => {
    console.log(error)
    throw new Error(error)
  })
}

and this works! Thanks to Christa for the answer that got me to the problem between axios and flask.

Thome
  • 31
  • 7