Search code examples
csrfflask-wtforms

CSRF Session token missing error. Expressjs application using axios. Not using a form


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.


Solution

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