Axios PUT Request 403 Forbidden when logged into Django - django

Whenever I am logged into a django user and I try and send a PUT request to my URL I get a 403 Forbidden Error. However, it works both when I am not logged in and also from the Django Rest API client.
Here is my code in my frontend:
let parameters = `user/${userID}/`
return new Promise((resolve, reject) => {
axios({
method: 'PUT',
url: 'http://127.0.0.1:8000/' + parameters,
data: updatedUser,
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => {
resolve(response)
})
.catch(error => {
console.log(error)
// reject(error)
})
});
I am very confused as I can't see the difference when I am logged into a django user and when I am not, as nothing changes in the frontend. Thanks
EDIT:
This is in my urls.py
path('user/<id>/', views.RetrieveUpdateDestroyUser.as_view()),
And this is the view:
class RetrieveUpdateDestroyUser(RetrieveUpdateDestroyAPIView):
"""
View to handle the retrieving, updating and destroying of a User.
This View will also log any changes made to the model.
"""
serializer_class = UserCreateUpdateSerializer
queryset = CustomUser.objects.all()
lookup_field = 'id'
permission_classes = (AllowAny,)
def update(self, request, *args, **kwargs):
"""
PUT and UPDATE requests handled by this method.
"""
return super().update(request, *args, **kwargs)
I have also tested doing POST and PUT when I am logged into a user and they don't work, but GET does. Thanks
Also tried disabled CSRF but to no avail either

Writing this answer to summarize what we have discovered.
The problem: the AJAX (PUT) call to the DRF endpoint fails with 403 HTTP error for authenticated users and works just fine for anonymous users
Desired Behaviour: make that call working for both anonymous and authenticated users
Reason: by default DRF perform CSRF check for unsafe HTTP methods (POST, PUT, PATCH and DELETE) https://www.django-rest-framework.org/topics/ajax-csrf-cors/
Possible Solutions:
Disable CSRF check like described here https://stackoverflow.com/a/30875830/764182
Pass CSRF token within the PUT request. For more information about CSRF + AJAX in Django read here https://docs.djangoproject.com/en/3.1/ref/csrf/#ajax. For Axios and default Django settings the solution might be:
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN";
axios.defaults.xsrfCookieName = "csrftoken";
axios.defaults.withCredentials = true;

Related

Simulate CSRF attack in Django Rest Framework

I'm trying to get an understanding of how CSRF tokens work, currently my goal is to create a situation where the CSRF attack is possible. I'm hosting two Django apps locally on different ports. I access one by localhost:8000, the other by 127.0.0.1:5000 -- that ensures cookies are not shared between apps.
There's an API view
class ModifyDB(APIView):
def post(self,request,format=None):
if request.user.is_authenticated:
return Response({'db modified'})
else:
return Response({'you need to be authenticated'})
which shouldn't be accessed by unauthenticated users.
The "malicious" site has a button that's supposed to trigger the attack when a user is logged on the target site:
const Modify = () => {
const onClick = async e => { e.preventDefault();
const instance = axios.create({
withCredentials: true,
baseURL: 'http://localhost:8000',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
})
const res = await instance.post('/api/modifydb');
return res.data
}
return (
<button class = 'btn' onClick = {onClick}> send request </button>
)
}
My authentication settings are as follows:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'my_proj.settings.CsrfExemptSessionAuthentication',
],
}
where CsrfExemptSessionAuthentication is a custom class that disables csrf protection for my educational purposes:
class CsrfExemptSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
return
django.middleware.csrf.CsrfViewMiddleware is also disabled.
Both CORS_ALLOW_ALL_ORIGINS and CORS_ALLOW_CREDENTIALS are set to true. My question is: what am I doing wrong, because when I'm logged in on the attacked site this axios request from the malicious site returns "you need to be authenticated" response.
I expected that somehow sessionid cookie would be included and request would be successful (which is the logic behind this type of attack if I'm understanding it correctly).
UPDATE
I tried to use a form
<form action="http://localhost:8000/api/modifydb" method="POST">
<input type="submit" value="make a malicious request"/>
</form>
instead of the axios request. It works with this additional setting:
SESSION_COOKIE_SAMESITE = None
while my previous code for request still doesn't work even with this new setting.
Now, I don't get what is the difference between POST requests from a form and from axios regarding cookies attachment. And does SESSION_COOKIE_SAMESITE if enabled prevents this type of attack completely?

Sending data from React form to Django Rest Framework without a Model

I am trying to send some data from my React frontend to my Django REST backend.
My Django application does NOT have models.
I have a react form. When the user submit the button, the form "submits" three params:
firmNAme
timeframes
numberOfResults
I want to send these three parameters to my Django backend. To do so, I m assuming React is sending a "POST" request to the Django endpoint.
I need these params in Django so that I can use them to do some manipulations for a ML model.
My React function
handleSubmit(event){
event.preventDefault()
fetch("/myapi/getreactinfotwitter",{
method:"POST",
headers:{
'Accept':'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
'data_options': {
'partnerName': this.state.firmName,
'timeframes': this.state.timeframes,
'numberOfResults': this.state.numberOfResults,
}
})
})
Now, I think the the Reast "POST" has to be matched with a Django "GET", so that Django gets the three params.
In DJANGO I have:
urls.py
path('getreactinfotwitter/', getreactinfotwitter, name="getreactinfotwitter")
myapi/views.py
#api_view(['GET', 'POST'])
def getreactinfotwitter(request):
print(request)
if request.method == 'GET':
return Response(request.data)
HOWEVER
My request in the views.py is not getting anything.
In the web console, when I click submit I get POST http://localhost:8080/myapi/getreactinfotwitter/ 500 (Internal Server Error)
Here are the steps to configure your React front-end with Django REST backend.
Django Settings:
Install django-cors-headers with the following command:
pip install django-cors-headers
Add it to your project settings.py file:
INSTALLED_APPS = (
##...
'corsheaders'
)
Next, you need to add corsheaders.middleware.CorsMiddleware to the middleware classes in settings.py
MIDDLEWARE = [
# corsheaders middleware
'corsheaders.middleware.CorsMiddleware',
...
]
You should allow the hosts in settings.py from where you want to get data. At the initial stage allow *
ALLOWED_HOSTS = [*]
React Settings:
Install axios using the following command:
npm i axios
Now follow the example in below for the React class.
import React from "react";
...
import Axios from "axios";
...
yourFunctionName = () => {
Axios.post(`URL`, {
'partnerName': this.state.firmName,
'timeframes': this.state.timeframes,
'numberOfResults': this.state.numberOfResults,
},
{
headers: {
"Authorization": `AUTHORIZATION_KEY`,
"Content-Type": 'application/json'
}
}
)
.then(res => console.log(res))
.catch(error => console.err(error))
}
Fatima's answer is excellent enough. I am adding here some extra thing that you wanted to know in comment.
Basically you don't need to catch the data manually and save it into database. Django gives us extreme flexibility. If your model, serializer and view are like below then any axios request will be handled by django itself (i.e, django catches the data and saves it into database), you don't need to write code for manually save it into database.
Model
class Partner(models.Model):
partnerName = models.CharField(max_length=100)
timeFrames = models.IntegerField()
numberOfResults = models.IntegerField()
Serializer
class PartnerSerializer(serializer.ModelSerializer):
class Meta:
model=Partner
fields = '__all__'
Views
class PartnerView():
queryset = Partner.object.all()
serializer_class = PartnerSerializer
If you still want to catch the data inside the view and modify in your way then just add built in function create() (builtin function of ModelViewSet) in your view class. If you want to modify the data inside serializer instead of view then add the builtin create() function (built in function of ModelSerializer) in your serializer.
The mentioned built in function (and also many more built in function) can be found in this website http://www.cdrf.co/
Exact url of create() function of view is this and
Exact url of create() function of serializer is this
See the code below of customization.
Customized Views
class PartnerView():
queryset = Partner.object.all()
serializer_class = PartnerSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# Here all incoming data we kept in serializer variable.
# Change the data in your way and then pass it inside perform_create()
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(
data={
"status": 201,
"message": "Product Successfully Created",
"data": serializer.data,
},
status=status.HTTP_201_CREATED,
headers=headers
)
If you want to modify the data inside the serializer then use the below code.
Customized Serializer
class PartnerSerializer(serializer.ModelSerializer):
class Meta:
model = Partner
fields = '__all__'
def create(self, validated_data):
my_incoming_data = validated_data
# If you want to pop any field from the incoming data then you can like below.
# popped_data = validated_data.pop('timeFrames')
inserted_data = Partner.objects.create(**validated_data)
return Response(inserted_data)
if you are trying to send data with react and fetch to an APIView in Django, the only thing you need to do is to remove the Content-Type header from fetch headers, so the DRF would set it, and you can access files and data from request.data.
DRF APIView:
class ImportProductsAPI(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated, IsAdmin)
def put(self, request):
# you can pass other data as well as files too.
site_id = request.data["site_id"]
# and there is the file you want
file = request.data["excel"]
return JsonReponse()
Fetch (in react project)
let formData = new FormData();
formData.append("excel", file);
formData.append("site_id", site_id);
fetch(
"api/for/upload",
{
method: "PUT",
headers: {
Accept: "application/json",
Authorization: "token thisisthetokenforthisuser",
// consider that there is no Content-Type being setted.
},
body: formData,
})
.then((response) => {
return response.json();
})
.then((response) => {
console.log(response);
})
.catch((error) => {
console.error(error);
});

Django REST giving a 403 forbidden on DELETE methods, but not POST

I am trying to delete an entry using an ajax call.
when I am using the default 'delete' method from generics.DestroyAPIView, I am getting a 403 Forbidden, but if I add a post method, call the delete method immediately and change the ajax type to 'post' it works fine. Would anyone have an idea what causes this?
Note that I overwrite the get_object function to get the object based on posted data. (could it be due to delete methods not allowing to post data? If so, why? And how would you pass the CSRF token??)
ajax:
$.ajax({
url: '{% url "account:api:post_details_delete" %}',
type: 'delete',
data: { csrfmiddlewaretoken: "{{ csrf_token }}", name: json.name, value: json.value }
});
url:
path('post_details/delete/', PostDetailDeleteApiView.as_view(), name='post_details_delete'),
view:
class PostDetailDeleteApiView(generics.DestroyAPIView):
serializer_class = PostDetailSerializer
# the code below allows it to work if uncommented and type in ajax changed to 'post'
# def post(self, request, *args, **kwargs):
# return self.delete(request, *args, **kwargs)
def get_object(self):
"""
Returns the post detail object to be deleted based on the name:value pair
provided by the ajax call.
"""
data = self.request.data
obj = Post_Detail.objects.filter(
name=data.get('name', ''), value=data.get('value', '')
).filter(
post__account__user=self.request.user
)
if obj.exists():
return obj.get()
else:
raise Http404
serializer:
class PostDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Post_Detail
fields = ['name', 'value']
A 403 Forbidden error code would suggest that in some way or another that either permission is denied or that you're not authenticated.
The 'DestroyAPIView' is used for delete-only API endpoints for a single model instance.
Do you have the full traceback that you can append to your question?
Docs References
Django REST Framework's Destroy API View
Django REST Framework's Guide to Permissions
According to Django documentation you must pass the csrftoken to headers of ajax request
While the above method can be used for AJAX POST requests, it has some inconveniences: you have to remember to pass the CSRF token in as POST data with every POST request. For this reason, there is an alternative method: on each XMLHttpRequest, set a custom X-CSRFToken header (as specified by the CSRF_HEADER_NAME setting) to the value of the CSRF token. This is often easier because many JavaScript frameworks provide hooks that allow headers to be set on every request.

DRF - request.user returns AnonymousUser in APIView while logged in during api call from fetch

I've constructed a APIView as such:
class CustomAPIView(APIView):
def get(self, request, *args, **kwargs):
if not request.user or not request.user.is_authenticated():
return Response("User not logged in", status=status.HTTP_403_FORBIDDEN)
# Other stuff
And in my html template I'm making a call to it using fetchAPI:
fetch('/api/request/url/', {method: 'get'})
.then(
// Process info );
I'm logged in through all this, but I'm always being greeted with a 403 response with the request.user variable in the APIView returning AnonymousUser. However, if I try and visit the api url manually everything works out right.
Can someone point out what I'm missing?
Thanks in advance.
The issue with fetch api is that by defualt it will not send cookies to the server.
By default, fetch won't send or receive any cookies from the server,
resulting in unauthenticated requests if the site relies on
maintaining a user session (to send cookies, the credentials init
option must be set).
So You have to set credentials: 'same-origin' in your fetch request,
fetch('/api/request/url/', {method: "GET", credentials: 'same-origin'})
.then(
// Process info );
for cross-origin requests, use credentials: 'include'

django-rest-framework returning 403 response on POST, PUT, DELETE despite AllowAny permissions

I'm using a django-oneall to allow social login session authentication on my site. While it isn't one of the suggested auth providers for django-rest-framework, rest_framework.authentication.SessionAuthentication uses django's default session authentication. so I thought it should be fairly simple to integrate.
On the permissions side, ultimately I'll use IsAdmin, but for development purposes, I just had it set to IsAuthenticated. When that returning 403s, I relaxed the permissions to AllowAny, but still no dice. Here's my rest framework config:
settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
# 'rest_framework.permissions.IsAuthenticated',
# 'rest_framework.permissions.IsAdminUser',
),
'PAGE_SIZE': 100,
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.DjangoFilterBackend',
),
}
EDIT:
I got this working based on the answer below. It turns out that rest_framework expects both the csrftoken cookie and a a X-CSRFToken Header of the same value, I setup my front-end code to send that header for all ajax requests and everything worked fine.
Django REST Framework returns status code 403 under a couple of relevant circumstances:
When you don't have the required permission level (e.g. making an API request as an unauthenticated user when DEFAULT_PERMISSION_CLASSES is ('rest_framework.permissions.IsAuthenticated',).
When you doing an unsafe request type (POST, PUT, PATCH or DELETE - a request that should have side effects), you are using rest_framework.authentication.SessionAuthentication and you've not included your CSRFToken in the requeset.
When you are doing an unsafe request type and the CSRFToken you've included is no longer valid.
I'm going to make a few demo requests against a test API to give an example of each to help you diagnose which issue you are having and show how to resolve it. I'll be using the requests library.
The test API
I set up a very simple DRF API with a single model, Life, that contains a single field (answer, with a default value of 42). Everything from here on out is pretty straight forward; I set up a ModelSerializer - LifeSerializer, a ModelViewSet - LifeViewSet, and a DefaultRouter on the /life URL route. I've configured DRF to require user's be authenticated to use the API and to use SessionAuthentication.
Hitting the API
import json
import requests
response = requests.get('http://localhost:8000/life/1/')
# prints (403, '{"detail":"Authentication credentials were not provided."}')
print response.status_code, response.content
my_session_id = 'mph3eugf0gh5hyzc8glvrt79r2sd6xu6'
cookies = {}
cookies['sessionid'] = my_session_id
response = requests.get('http://localhost:8000/life/1/',
cookies=cookies)
# prints (200, '{"id":1,"answer":42}')
print response.status_code, response.content
data = json.dumps({'answer': 24})
headers = {'content-type': 'application/json'}
response = requests.put('http://localhost:8000/life/1/',
data=data, headers=headers,
cookies=cookies)
# prints (403, '{"detail":"CSRF Failed: CSRF cookie not set."}')
print response.status_code, response.content
# Let's grab a valid csrftoken
html_response = requests.get('http://localhost:8000/life/1/',
headers={'accept': 'text/html'},
cookies=cookies)
cookies['csrftoken'] = html_response.cookies['csrftoken']
response = requests.put('http://localhost:8000/life/1/',
data=data, headers=headers,
cookies=cookies)
# prints (403, '{"detail":"CSRF Failed: CSRF token missing or incorrect."}')
print response.status_code, response.content
headers['X-CSRFToken'] = cookies['csrftoken']
response = requests.put('http://localhost:8000/life/1/',
data=data, headers=headers,
cookies=cookies)
# prints (200, '{"id":1,"answer":24}')
print response.status_code, response.content
Just for anyone that might find the same problem.
If you are using viewsets without routers like:
user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})
Django Rest framework will return 403 unless you define permission_classes at a class level:
class UserViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing user instances.
"""
permission_classes= YourPermisionClass
Hope it helps!
For completeness sake, there is one more circumstance under which DRF returns code 403: if you forget to add as_view() to the view declaration in your urls.py file. Just happened to me, and I spent hours until I found where the issue was, so maybe this addition can save some time for someone.
For those that aren't able to even access their csrftoken from Javascript:
In my case I wasn't able to get the csrftoken from my Javascript code to be able to set it in my ajax POST. It always printed null. I finally discovered that the django CSRF_COOKIE_HTTPONLY environment variable was set to True.
From the Django Documentation
CSRF_COOKIE_HTTPONLY: If this is set to True, client-side JavaScript will not be able to access the CSRF cookie."
Changing CSRF_COOKIE_HTTPONLY to False allowed me to finally get the csrftoken.
https://docs.djangoproject.com/en/4.1/ref/settings/#std-setting-CSRF_COOKIE_HTTPONLY
One more situation that someone may find is that you get a 403 error on an AllowAny route when you pass an token as null in the "Authorization" header in your request. For example, you may want to allow anyone to use the route but also want to know if the person that used the route is an authenticated user.
E.g.
if (token) {
headers = {
"Content-Type": "application/json",
"Authorization": "Token " + token
}
} else {
headers = {
"Content-Type": "application/json"
}
}