I'm using Pyres workers to do some processing of data users enter in a form. Their processing is done by a view on my form, which I make a POST request to, with data including the data to process and a CSRF middleware token for the user. My issue is that this is apparently not enough, as Django still rejects my request with a 403 forbidden.
Relevant code:
Form handler:
def handler(request):
if(request.method == "POST"):
if(request.POST.__contains__("taskdata")):
#valid post of the form
taskdata = escape(request.POST.get("taskdata",""))
t = TaskData(data=taskdata, time_added=timezone.now(), token=request.POST.get("csrfmiddlewaretoken",""))
t.save()
r = ResQ(server="127.0.0.1:6379")
r.enqueue(TaskData, t.id)
return HttpResponse(t.id)
else:
#invalid post of the form
raise Http404
else:
raise Http404
Pyres worker job:
#staticmethod
def perform(taskData_id):
#Get the taskData from this id, test it for tasky stuff
task_data = TaskData.objects.get(pk=taskData_id)
post_data = [('id',task_data.id),('data',task_data.data), ('csrfmiddlewaretoken',task_data.token)] # a sequence of two element tuples
result = urllib2.urlopen('http://127.0.0.1:8000/tasks/nlp/process/', urllib.urlencode(post_data))
content = result.read()
return
View being posted to by that job:
def process(request):
if(request.method == "POST"):
return HttpResponse("HEY, it works!")
if(request.POST.__contains__("data") and request.POST.__contains__("id")):
#valid post to the form by the model
#taskdata = escape(request.POST.get("taskdata",""))
#data = get_times(taskdata)
return HttpResponse("Hey from process!")
#return HttpResponse(json.dumps(data))
else:
#invalid post of the form
raise Http404
else:
raise Http404
What I'm basically trying to do is save some raw data at form submission, along with the CSRF token for it. The workers then send that data + token to a processing view.
Unfortunately, posting the token doesn't seem to be enough.
Does anybody know what the csrf protection actually looks for, and how I can make my Pyres workers compliant?
(Suggested tag: pyres)
I think I see the problem.
The way Django's CSRF protection works is by generating a nonce, then setting a cookie to the value of the nonce, and ensuring the csrfmiddlewaretoken POST value matches the value of the cookie. The rationale is that it makes it a stateless system, which works without any persistent session data.
The problem is that the request you make in the Pyres worker job...
result = urllib2.urlopen('http://127.0.0.1:8000/tasks/nlp/process/',
urllib.urlencode(post_data))
...is coming from the server, not the client, so it won't have the cookie set.
Assuming the /tasks/nlp/process/ URL is protected such that it can only be accessed by the server, then it's probably simplest to make the process() view exempt from CSRF checking with...
#csrf_exempt
def process(request):
...
...otherwise you'll have to manually grab the cookie value in the handler() view, and pass it on to the Pyres worker job.
Update
To ensure the process() method can only be called by the server, one simple way would be to check the request object with something like...
#csrf_exempt
def process(request):
if request.META['REMOTE_ADDR'] != '127.0.0.1':
# Return some error response here.
# 403 is traditional for access denied, but I prefer sending 404
# so 'hackers' can't infer the existence of any 'hidden' URLs
# from the response code
raise Http404
# Now do the thing
....
...although there may be some built-in decorator or somesuch to do this for you.
Related
I have the following python code in my Django views.py, the code takes in a JSON body and send the extracted DATA to another API endpoint, I have simplified the code here.
How do I enable csrf such that it will send the token back to the caller for this method? I am calling this from postman.
#csrf_protect
def validate_booking(request):
if request.method != "POST":
return HttpResponseServerError("Invalid HTTP method")
body = json.loads(request.body)
booking_details = body["booking_details"]
DATA = {
"name": booking_details["name"],
"nric": booking_details["nric"],
"booking_id": booking_details["booking_id"]
}
return HttpResponse(status="200")
This site directs to put this piece of code in my method. But what is "a_template.html"?
https://docs.djangoproject.com/en/4.1/ref/csrf/
#csrf_protect
def my_view(request):
c = {}
# ...
return render(request, "a_template.html", c)
This isn't an easy thing to do as CSRF is 2 steps thing
There is a value that is passed to the client and it is saved to the session on the server.
When a POST request is received, the client shall send this as csrfmiddlewaretoken in the body and the server will check the value against the stored one in the server's session.
So this isn't feasible to be done in APIs as you require session Management which is not of REST API implementations.
Thanks for your reply. I managed to find a solution by doing the following:
Create a new GET method that will generate the session CSRF token using python
Instead of using render which expects a HTML template file, I used JsonResponse(data) to return in JSON format directly
In my postman app which I am making the POST request with the X-CSRFToken in the header, I will first make a GET request to the new method I created in step 1 to retrieve the token and store it as an environment variable
The following is the GET method sample:
from django.http import JsonResponse
def get_csrf_token(request):
csrf_token = csrf(request)['csrf_token']
data = {'csrf_token': csrf_token}
return JsonResponse(data)
I'm creating a Restful API using Django Rest Framework, i'm not serving sensitive data but still i wanted to add some sort of authorization system for viewing my API endpoints.
Basically each user has an API key assigned, and in order to view any endpoint, the user needs to provide the key when performing any request. All the endpoints use only GET to retrieve the data, so what i did is the following:
The API key is provided in the GET params, so something like myURL/api/endpoint/?key=1234&filter=test
A middleware checks if that API key exists in my database, and if it does the user is able to get the data.
Here is my middleware:
TOKEN_QUERY = "key"
class TokenMiddleware(AuthenticationMiddleware):
def process_request(self, request):
if request.user.is_authenticated:
return None
else:
try:
token = request.GET[TOKEN_QUERY]
except Exception as e:
# A token isn't included in the query params
return JsonResponse({'error': 'Missing parameter: make sure to include your key.'})
try:
query = API_keys.objects.get(api_token=token)
except:
token = None
if token != None:
return None
else:
return JsonResponse({'error': 'Authentication failed. Make sure to provid a valid API key.'})
This system works without any problem, but i'm concerned about safety. How safe is this? Should i not use a GET request (of course i'll make sure to use HTTPS and SSL) ? Or is there a de facto way to create this kind of system? Any kind of advice is appreciated.
You can try this
from rest_framework import permissions
TOKEN_QUERY = "key"
# guest token validation class
class GuestTokenPermission(permissions.BasePermission):
def __init__(self, allowed_methods):
self.allowed_methods = allowed_methods
def has_permission(self, request, view):
token = request.META.get('HTTP_GUEST_TOKEN', None)
if token == TOKEN_QUERY:
return request.method in self.allowed_methods
else:
if request.user.is_superuser:
return request.method in self.allowed_methods
# put where you want to set permission
permission_classes = (partial(GuestTokenPermission, ['GET', 'POST', 'HEAD']),)
Refer https://www.django-rest-framework.org/api-guide/permissions/
I use django, django rest framework and ember.js; my entire application thereforce communicates via ajax.
Authentication is done via oauth2 and a token is send in the headers within every request.
Everythings nice and shiny but file downloads.
At one point users can download a pdf and I don't know how to apply authentication there - because on the file download I cannot send and headers, it's just a link.
I thought of adding SessionAuthentication to that particular rest api call, but the session always flags the incoming user as anyonymous.
How can I force django to create a session on top of the oauth2 token flow?
I tried login(request, user), but it somehow does not kick in.
I ended up with signed tickets, e.g. i send back a token, that is able to bypass auth for a defined timeframe. Therefore the ajax app can first request the token and then fire again a standard get request with the token attached.
Here's the basic idea, that I mixin to views:
class DownloadableMixin():
"""
Manages a ticket response, where a ticket is a signed response that gives a user limited access to a resource
for a time frame of 5 secs.
Therefore, file downloads can request a ticket for a resource and gets a ticket in the response that he can
use for non-ajax file-downloads.
"""
MAX_AGE = 5
def check_ticket(self, request):
signer = TimestampSigner()
try:
unsigned_ticket = signer.unsign(request.QUERY_PARAMS['ticket'], max_age=self.__class__.MAX_AGE)
except SignatureExpired:
return False
except BadSignature:
return False
if self.get_requested_file_name() == unsigned_ticket:
return True
return False
def get_ticket(self):
signer = TimestampSigner()
return signer.sign(self.get_requested_file_name())
def has_ticket(self, request):
return 'ticket' in request.QUERY_PARAMS
def requires_ticket(self, request):
return 'download' in request.QUERY_PARAMS
def get_requested_file_name(self):
raise NotImplementedError('Extending classes must define the requested file name.')
I'm trying to programmatically POST rather than using a web form. To do this, I'm using the MultipartPostHander module downloaded from https://pypi.python.org/pypi/MultipartPostHandler/.
It seems to work well (ie. does the same thing that a web form POST request would do), except users.get_current_user() returns None in the POST page handler. In comparison, the same POST page handler returns the user info correctly, if the POST request is submitted by a web form.
Also tried with poster (http://atlee.ca/software/poster/index.html) and had the exact same issue.
Why could this be happening? Thank you in advance.
class first_page_handler(basehandler.BaseHandler):
def get(self):
user = users.get_current_user()
if user:
raw_upload_url = webapp2.uri_for('test_module')
qimage_upload_url = r'http://localhost:8080'+raw_upload_url
params = {'param1': 'param'}
opener = urllib2.build_opener(multipost.MultipartPostHandler)
urllib2.install_opener(opener)
req = urllib2.Request(qimage_upload_url, params)
response = urllib2.urlopen(req) # <<<< POST request submitted
text_response = response.read().strip()
return self.response.write("<html><body><p>"+text_response+"</p></body>
</html>")
else:
login_url = users.create_login_url("/admin")
context = {'login_url': login_url}
self.render_response('admin/login.html', **context)
class post_page_hander(basehandler.BaseHandler):
def post(self):
user = users.get_current_user() # << Returns NONE
return self.response.write("<html><body><p>"+user.email()+"</p></body></html>")
You're not setting any cookies on the request, so the server has no idea what the current user is.
(What you're doing is unusual ... there is probably a better way of doing whatever it is you want to do.)
I'm passing in a csrf_token for every post and xhr request and want to validate the token against the session csrf token. If they don't match, I throw a 401.
I've used the NewResponse subscriber in pyramid to inspect the request and validate the csrf token in the request params against the token in the session. The validation works but it still calls the view so it def does not work as it should.
Any suggestions on the proper way to do this?
#subscriber(NewResponse)
def new_response(event):
"""Check the csrf_token if the user is authenticated and the
request is a post or xhr req.
"""
request = event.request
response = event.response
user = getattr(request, 'user', None)
# For now all xhr request are csrf protected.
if (user and user.is_authenticated()) and \
(request.method == "POST" or request.is_xhr) and \
(not request.params.get('csrf_token') or \
request.params.get('csrf_token') != unicode(request.session.get_csrf_token())):
response.status = '401 Unauthorized'
response.app_iter = []
The NewResponse subscriber is called after your view is invoked.
You want to be using an event that is invoked earlier, for example NewRequest or ContextFound. In Pyramid 1.0, you'll need to use ContextFound to properly handle things because you cannot raise exceptions in NewRequest events (this is fixed in 1.1).
The way to do this with a ContextFound event is to register an exception view for HTTPException objects like this:
config.add_view(lambda ctx, req: ctx, 'pyramid.httpexceptions.HTTPException')
Basically this will return the exception as the response object when you raise it, which is perfectly valid for HTTPException objects which are valid Pyramid Response objects.
You can then register your event and deal with the CSRF validation:
#subscriber(ContextFound)
def csrf_validation_event(event):
request = event.request
user = getattr(request, 'user', None)
csrf = request.params.get('csrf_token')
if (request.method == 'POST' or request.is_xhr) and \
(user and user.is_authenticated()) and \
(csrf != unicode(request.session.get_csrf_token())):
raise HTTPUnauthorized
Pyramid contains its own CSRF validation, which is probably a better choice.
Given your session stored CSRF tokens, this would result in the following configuration:
from pyramid.csrf import SessionCSRFStoragePolicy
def includeme(config):
# ...
config.set_csrf_storage_policy(SessionCSRFStoragePolicy())
config.set_default_csrf_options(require_csrf=True)