Simulate CSRF attack in Django Rest Framework - django

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?

Related

CSRF token missing error using vue-recaptcha and a Django Rest Framework middleware

I have the following setup :
a frontend app developed with VueJS on which I have a registration form component, including few standard fields, a mandatory checkbox (user agreement), and a Google Recaptcha V2 (vue-recaptcha) checkbox
a backend API developed with Django Rest Framework, with a specific route to request Google Recaptcha web service (using rest_framework_recaptcha application)
Route declaration in urls.py
urlpatterns = [
...
path('recaptcha_verify/', RecaptchaVerifyView.as_view(), name='recaptcha_verify'),
...
]
DRF view :
class RecaptchaVerifyView(APIView):
allowed_methods = ["POST"]
def post(self, request, *args, **kwargs):
serializer = ReCaptchaSerializer(data=request.data)
if serializer.is_valid():
return Response({'success': True}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Simple serializer based on rest_framework_recaptcha:
from rest_framework_recaptcha.fields import ReCaptchaField
class ReCaptchaSerializer(serializers.Serializer):
token = ReCaptchaField()
Frontend VueJS component :
<v-form>
<v-checkbox
v-model="consentCheckbox"
:rules="[rules.required]"
>
<template v-slot:label>
<div>User agreement message</div>
</template>
</v-checkbox>
<div align="center">
<vue-recaptcha
ref="recaptcha"
#verify="onVerify"
:sitekey="sitekey"
:loadRecaptchaScript="true"
>
</vue-recaptcha>
</div>
</v-form>
And the methods associated to the component (handleRegister is called when the submit button is clicked) :
onVerify(response) {
this.message = false;
this.recaptchaResponse = response;
this.recaptchaVerified = true;
},
async handleRegister() {
// Check if recaptcha has been ticked
if (!this.recaptchaVerified) {
return true; // prevent form from submitting
}
else if ( this.$refs.form.validate() ) {
// Check recaptcha by calling backend (DRF) specific route (itself calling Google API)
var response = await RecaptchaService
.verifyRecaptcha(this.recaptchaResponse)
.catch(error => this.message = (error.response && error.response.data) || error.message || error.toString());
if (response.data.success) {
// process
}
}
Finally, the VueJS service dedicated to calling recaptcha API on backend:
import axios from 'axios';
const API_URL = process.env.VUE_APP_API_HOST_NAME + '/api/v1/accounts/';
class RecaptchaService {
async verifyRecaptcha(token) {
return axios.post(API_URL + 'recaptcha_verify/',
{
token: token,
})
.then((response) => {
return response;
})
.catch((error) => {
//catch error
}
I don't have any issue working in a development environment (frontend and backend accessible on localhost). Yet, when deployed in production (with https enabled), I face the following problem:
Checking the user agreement checkbox first, then the Recaptcha works fine
Checking the Recaptcha then the user agreement returned a 403 Forbidden error, with the message CSRF token missing or incorrect
Once I get the previous error, I always get the error when filling the form again, no matter the order of checking boxes
I don't get why:
the problem does not occur on localhost
the order of filling form lead to different behaviours
the problem only occurs on my registration form while I do have several other forms (login, sending data, etc.) that work fine
Any ideas to help me understand/investigate the problem ? I have read many posts related to Django and CSRF or CORS related issues but they are often related to django templates or SessionAuthentication.
It's not easy to guess the problem without seeing the code.
However, since localhost works maybe the base url in production in your frontend is not https:// but http://.

Axios PUT Request 403 Forbidden when logged into 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;

How to add change password functionality to Django / React app

I'm trying to add change password functionality to a Django / React app. I'm struggling to find any way to make this work. I'm using django-rest-auth and django-rest-framework.
I would be happy to either submit the password change request from a React page, or else to redirect the user to the native Django template-driven page - leaving my SPA, but acceptable to get the functionality working.
However, the change password page requires the user to be logged in, and I can't see how to do this from the React app. I've set up the Django page with a custom template, but if I link to it from the app then the URL redirects to login:
If I copy the request to cURL and run it in a terminal, I see this error:
# api/urls.py
from django.urls import include, path
from django.contrib.auth import views
from django.conf.urls import include, url
urlpatterns = [
...
path('password_change/', views.PasswordChangeView.as_view(template_name='account/password_change.html'), name='password_change'),
]
account/password_change.html
{% extends 'account/base.html' %}
{% block body_block %}
<h1>Change your password</h1>
<form method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit" />
</form>
{% endblock %}
I log in to my app and then navigate to where the change password page should be:
http://localhost:8000/api/v1/password_change/
This is then redirected to:
http://localhost:8000/accounts/login/?next=/api/v1/password_change/
My guess is that this is because the endpoint requires authentication and it's not been provided. But although I have a stored authentication token, I can't find out how to send this as part of the weblink and previous experience with reset password suggests that it's not the token Django would be looking for from a template.
I've hunted for ages and can't find any example of how to add change password to a React app. Surely this must be something every React app needs? What basic thing am I missing?
Edit: thanks to Rik Schoonbeek for the answer, which is that the endpoint supplied by djang-rest-auth can be used directly. Somehow I missed this in all my confusion. In case it helps anybody else, here is sample working code that uses JavaScript fetch and hard-coded passwords instead of data from the form, just to demonstrate the process.
This code also demonstrates detecting an error in the fetch command, which doesn't work quite like you might expect. There are probably better ways to do it, but this does at least give you a hook.
Working code:
export const changePassword = () => (dispatch) => {
const token = localStorage.getItem('jwtToken');
const data = {
'new_password1': 'othertext',
'new_password2': 'othertext',
'old_password': 'sometext'
};
var formData = new FormData();
// Push our data into our FormData object
for(var name in data) {
formData.append(name, data[name]);
}
const headers = {
'Authorization': `Token ${token}`,
};
return fetch('/api/v1/rest-auth/password/change/', {
headers,
'method': 'POST',
'body': formData,
})
.then(res => {
if(res.ok) {
console.log('successfully changed password');
return res.json();
} else {
console.log('error changing password');
}
})
.then(res => {
console.log('Change password res ', res);
});
};
And important! add this to settings.py, otherwise the password will be changed regardless of whether the old password is correct:
OLD_PASSWORD_FIELD_ENABLED = True
And if you want the user to stay logged in after they have reset their password, add this too:
LOGOUT_ON_PASSWORD_CHANGE = False
If you use token authentication, like I do, you can authorize the user by sending the token in the header of the request to change the password. You need to add "Token " in front of the token though (see example below).
This is how I did it, using axios, and token authentication.
const response = await axios.post(
"http://127.0.0.1:8000/rest-auth/password/change/",
values,
{
headers: { Authorization: "Token " + this.props.loginToken }
}
);
The password change endpoint requires the following data to be sent:
new_password1
new_password2
old_password
As explained here in the django-rest-auth docs
So, the "values" parameter in my above axios request is a json containing those values.

Making axios request to django

I have a complex problem in sending and receiving data in react to django with axios.
I'm not using REST API. This is my Handel function which is related with my signup form tag and after each click on submit button this function executes:
HandelSignUp(e){
e.preventDefault();
let Username = this.refs.username.value;
let Pass = this.refs.pass.value;
let Email =this.refs.email.value;
axios({
url:'http://127.0.0.1:8000/signupAuth/',
mothod:'post',
data:{
username:this.Username,
pass:this.Pass,
email:this.Email
},
headers: {
"X-CSRFToken": window.CSRF_TOKEN,
"content-type": "application/json"
}
}).then(respons =>{
console.log(respons);
})
.catch(err =>{
console.log(err);
});
and also this is my django urls.py :
urlpatterns = [
path('signupAuth/',ReactApp_View.signupRes),
]
ReactApp_View is my views.py in ReactApp file which I imported correctly.
ok now let's see my views.py:
def signupRes(request):
body_unicode = request.body.decode('utf-8')
data = json.loads(myjson)
return HttpResponse(request.body)
after all when I fill my signup fields and then click on button I see this massage in console log of my browser:
Failed to load http://127.0.0.1:8000/signupAuth/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access.
What should I do?
and an extra question: what happen in the given url in my axios?
just open your website with the same host url you are trying to call. Use http://127.0.0.1:8000/ instead of localhost

AngularJS + Django Rest Framework + CORS ( CSRF Cookie not showing up in client )

I am developing a 1-page application in AngularJS using and Django Rest Framework + Django CORS Headers.
My problem is that the "csrftoken" cookie never shows up in my browser when I have contacted the backend.
For example: I am doing a login using a post. I get the "sessionid" cookie properly but the "csrftoken" never shows up and therefor I cannot do proper posts from my client since I will get denied due the lack of the csrf token.
I have analyzed the response headers from the API and the csrftoken is not ther.
I have looked directly in the rest API browser and it shows up fine there.
Just to point out, I can do my first POST to login since Django Rest Framework only forces CSRF for authenticated users. If I try to relogin it will fail since the "sessionid"-cookie it present.
I am not interessted in bypassing the CSRF protection as some posts on stackoverflow suggests.
Some code snippets from front/backend. These are unfinnished snippets, so dont get hung up on poorly written code.
Backend API LoginView
class LoginView(APIView):
renderer_classes = (JSONPRenderer, JSONRenderer)
def post(self, request, format=None):
serializer = LoginSerializer(data=request.DATA)
if serializer.is_valid():
userAuth = authenticate(username=serializer.data['username'], password=serializer.data['password'])
if userAuth:
if userAuth.is_active:
login(request, userAuth)
loggedInUser = AuthUserProfile.objects.get(pk=1)
serializer = UserProfileSerializer(loggedInUser)
user = [serializer.data, {'isLogged': True}]
else:
user = {'isLogged': False}
return Response(user, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Client side AngularJS Login Controller
.controller('LoginCtrl', ['$scope', '$http', 'uService', '$rootScope', function(scope, $http, User, rootScope) {
scope.login = function() {
var config = {
method: 'POST',
withCredentials: true,
url: rootScope.apiURL+'/user/login/',
data : scope.loginForm
};
$http(config)
.success(function(data, status, headers, config) {
if (status == 200) {
console.log(data[0]); //Test code
// succefull login
User.isLogged = true;
User.username = data.username;
}
else {
console.log(data); //Test code
User.isLogged = false;
User.username = '';
}
})
.error(function(data, status, headers, config) {
console.log('Testing console error');
User.isLogged = false;
User.username = '';
});
};
}]);
Anyone with any good tips/ideas/examples?
AngularJS Single Page Web Application on Sub-domain A, talking to a Django JSON (REST) API on Sub-domain B using CORS and CSRF protection
Since I'm currently working on a similar setup and was battling to get CORS to work properly in combination with CSRF protection, I wanted to share my own learnings here.
Setup - The SPA and the API are both on different sub-domains of the same domain:
AngularJS (1.2.14) Single Page Web Application on sub-domain app.mydomain.com
Django App (1.6.2) implements a JSON REST API on sub-domain api.mydomain.com
The AngularJS app is served through a Django App in the same project as the Django API APP such that it sets a CSRF Cookie. See, for instance, also How to run multiple websites from one Django project
Django API App - In order to get CORS and CSRF protection working I needed to do the following at the API backend.
In settings.py for this app (an extension of the Django project settings.py):
Add the corsheaders app and middleware and the CSRF middleware:
INSTALLED_APPS = (
...
'corsheaders',
...
)
MIDDLEWARE_CLASSES = (
...
'django.middleware.csrf.CsrfViewMiddleware',
...
'corsheaders.middleware.CorsMiddleware',
)
Also see Django CORS headers on GitHub
Add the domain for the SPA Webapp to the CORS_ORIGIN_WHITELIST
CORS_ORIGIN_WHITELIST = [
...
'app.mydomain.com',
...
]
Set CORS_ALLOW_CREDENTIALS to True. This is important, if you don't do this, no CSRF cookie will be sent with the request
CORS_ALLOW_CREDENTIALS = True
Add the ensure_csrf_cookie decorator to your views handling the JSON API requests:
from django.views.decorators.csrf import ensure_csrf_cookie
#ensure_csrf_cookie
def myResource(request):
...
Django App for AngularJS - The AngularJS app is served through a Django App in the same project. This Django App is set-up to set a CSRF Cookie. The CSRF token from the cookie is then used for requests to the API (which thus runs as a part of the same Django project).
Note that almost all files related to the AngularJS application are just static files from the Django perspective. The Django App only needs to serve the index.html to set the cookie.
In settings.py for this app (again an extension of the Django project settings.py), set the CSRF_COOKIE_DOMAIN such that subdomains can also use them:
CSRF_COOKIE_DOMAIN = ".mydomain.com"
In views.py, I only need to render the AngularJS index.html file, again using the ensure_csrf_cookie decorator:
from django.shortcuts import render
from django.views.decorators.csrf import ensure_csrf_cookie
# Create your views here.
#ensure_csrf_cookie
def index(request):
return render(request, 'index.html')
Sending requests to the API using AngularJS - In the AngularJS App config set the following $httpProvider defaults:
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken';
$httpProvider.defaults.withCredentials = true;
Again, take note of the withCredentials, this ensures that the CSRF Cookie is used in the request.
Below I show how you can make requests to the api using the AngularJS $http service and JQuery:
$http.post("http://api.mydomain.com/myresource", {
field1 : ...,
...
fieldN : ...
}, {
headers : {
"x-csrftoken" : $cookies.csrftoken
}
});
Also see ngCookies module.
Using JQuery (1.11.0):
$.ajax("http://api.mydomain.com/myresource", {
type: 'POST',
dataType : 'json',
beforeSend : function(jqXHR, settings) {
jqXHR.setRequestHeader("x-csrftoken", get_the_csrf_token_from_cookie());
},
cache : false,
contentType : "application/json; charset=UTF-8",
data : JSON.stringify({
field1 : ...,
...
fieldN : ...
}),
xhrFields: {
withCredentials: true
}
});
I hope this helps!!
Directly from the docs https://docs.djangoproject.com/en/1.9/ref/csrf/#ajax
If your view is not rendering a template containing the csrf_token
template tag, Django might not set the CSRF token cookie. This is
common in cases where forms are dynamically added to the page. To
address this case, Django provides a view decorator which forces
setting of the cookie: ensure_csrf_cookie().
Since your application is a single-page application, you can add ensure_csrf_cookie() to the view that is responsible for the initial page load.
So I found my own solution to this, seems to work great.
This is the new snippets of my code:
Backend API LoginView ( added a decorator forcing the csrf token to be added to the body )
class LoginView(APIView):
renderer_classes = (JSONPRenderer, JSONRenderer)
#method_decorator(ensure_csrf_cookie)
def post(self, request, format=None):
c = {}
c.update(csrf(request))
serializer = LoginSerializer(data=request.DATA)
if serializer.is_valid():
userAuth = authenticate(username=serializer.data['username'], password=serializer.data['password'])
if userAuth:
if userAuth.is_active:
login(request, userAuth)
loggedInUser = AuthUserProfile.objects.get(pk=1)
serializer = UserProfileSerializer(loggedInUser)
user = [serializer.data, {'isLogged': True}]
else:
user = {'isLogged': False}
return Response(user, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
AngularJS Client side ( add token to the request header )
$http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
Server side settings file ( Specificly for django-cors-headers )
First 5 are added by default, but you need to add "X-CSRFToken" to allow such a header from the client to the API using CORS, else the post will be denied.
CORS_ALLOW_HEADERS = (
'x-requested-with',
'content-type',
'accept',
'origin',
'authorization',
'X-CSRFToken'
)
Thats it!
A small update to this solution.
Since AngularJS 1.2.10 you need to set the CSRF cookie for each request type in the client:
$http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
$http.defaults.headers.put['X-CSRFToken'] = $cookies.csrftoken;
$http.defaults.headers['delete']['X-CSRFToken'] = $cookies.csrftoken;
This due the following change that happened between 1.2.9 and 1.2.10
https://github.com/cironunes/angular.js/commit/781287473bc2e8ee67078c05b76242124dd43376
Hope this helps someone!
After So Much Search i landed on this solution and its work form me on local system and also on live web faction server this is my solution for Django users please go to your apache folder located in project then in bin you find
httpd.conf or your server config for php or other users (usually located in a *.conf file, such as httpd.conf or apache.conf), or within a .htaccess. then just add this code
<IfModule mod_headers.c>
SetEnvIf Origin (.*) AccessControlAllowOrigin=$1
Header add Access-Control-Allow-Origin %{AccessControlAllowOrigin}e env=AccessControlAllowOrigin
Header set Access-Control-Allow-Credentials true
</IfModule>
then in angular js app you just needed to place
angular.module('app', ['ngCookies'])
.config([
'$httpProvider',
'$interpolateProvider',
function($httpProvider, $interpolateProvider, $scope, $http) {
$httpProvider.defaults.withCredentials = true;
$httpProvider.defaults.xsrfCookieName = 'csrftoken';
$httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
}]).
run([
'$http',
'$cookies',
function($http, $cookies) {
$http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken;
}]);
Its Worked for me on Django Angularjs platform.
https://gist.github.com/mlynch/be92735ce4c547bd45f6