CSRF Token Checks Prohibit POST Rest API Calls for a Logged in User

I am trying to make a POST Rest API call to the following custom method using a web browsing session which was logged in manually through the standard login page:

@frappe.whitelist()
def download_test():
	frappe.local.response.filename = "test.txt"
	with open("/tmp/test.txt", "rb") as fileobj:
		filedata = fileobj.read()
	frappe.local.response.filecontent = filedata
	frappe.local.response.type = "download"

If I use the GET method it works. But If I use the POST method it prompts with “Server Error”.

I’ve done some deeper investigation and found that in frappe/auth.py under validate_csrf_token, the following is blocking the request:

if frappe.local.session.data.csrf_token != csrf_token:
	frappe.local.flags.disable_traceback = True
	frappe.throw(_("Invalid Request"), frappe.CSRFTokenError)

I’m not very knowledgeable with regards to CSRF but I’m just thinking since it has been authenticated and we permit GET API requests, should we not allow POST API requests as well?

@bohlian can you share the code that calls this method?

We have only added CSRF token validation for POST requests because that’s the most dangerous request.

If you are using an existing session of a logged in user, also send the csrf token via request header: X-Frappe-CSRF-Token

You will find the csrf token in frappe.local.session.data.csrf_token

@anand, thanks very much. That worked.

I have another issue which I am struggling with:

I have changed the API to the following that accepts 1 parameter:

@frappe.whitelist()
def download_test(test):
	frappe.local.response.filename = test + ".txt"
	with open("/tmp/test.txt", "rb") as fileobj:
		filedata = fileobj.read()
	frappe.local.response.filecontent = filedata
	frappe.local.response.type = "download"

I cannot seem to pass the parameter via an XHR call

var data0 = {"test": "1"};
var json = JSON.stringify(data0); 

var xhr = new XMLHttpRequest();
xhr.open("POST", '/api/method/frappe.templates.pages.print.download_test');
xhr.setRequestHeader("X-Frappe-CSRF-Token", frappe.csrf_token);
xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');

xhr.onload = function(success) {
    do something.....
};
xhr.send(json);

I have tried to send the params via a simple string test=1, I have tried a json format, I have tried a stringyfied version of json (as per the example above), but the querystring doesn’t get passed through. Is the API expecting the params to be in another format? I know using Ajax and specifying a data object works but I really need to use XHR.

1 Like

You cannot use “query string” in a POST request. You need to pass POST arguments instead.

This example might help: javascript - Send POST data using XMLHttpRequest - Stack Overflow

I would recommend using $.ajax instead, makes it very easy!

1 Like

@anand actually it is possible to pass parameters via POST which gets converted to a query string if I am not wrong. But it is quite strange since my previous example didn’t work, I tried investigating and only got as far as to seeing that the params don’t get passed to request.form. I abandoned this idea and went on to looking at other possibilities.

And yes the post you provided is right. I actually got it working with the same solution. Although I had to blob it out as an xml else it would reject the form. Thanks very much.

I agree with @anand though, $ajax is much simpler but certain things cannot be done via $ajax as far as my research goes, e.g. spitting the output as an arraybuffer.

So for anyone who wants to achieve the same thing you can use the following as an example:

var formData = new FormData();

formData.append("test", "1234");
var blob = new Blob([], { type: "text/xml"});
formData.append("webmasterfile", blob);

var xhr = new XMLHttpRequest();
xhr.open("POST", '/api/method/frappe.templates.pages.print.download_test');
xhr.setRequestHeader("X-Frappe-CSRF-Token", frappe.csrf_token);

xhr.onload = function(success) {
    if (this.status === 200) {
        do something....
    }
};
xhr.send(formData);
2 Likes

Hi,

How do you access the frappe.local.session.data.csrf_token?

I have added this line to the script -

xhr.setRequestHeader(“X-Frappe-CSRF-Token”, frappe.csrf_token);

and not sure how to set the token.

Thanks,

Payjer

Hi,

Figured it out.

You need to whitelist the token by adding the following code in init.py.

import frappe
import frappe.sessions
@frappe.whitelist(allow_guest=True)
def token():
return frappe.local.session.data.csrf_token

Once you do the above, you can get the token through REST API Get call.

Hope above helps someone.

Cheers,

Payjer

2 Likes

where do u find init.py file ?!

Maybe payjerwu intended this?
frappe@erpnext:~/frappe-bench$ find . -name __init__.py

edit: underscores missing - use backticks to _init.py_ solve problem!?

Fabulous!

can you tell what path sir? or folder