I'm writing a chrome extension that use GET and POST ajax calls to an api made with Django and Tastypie.
GET ajax calls are successful and I can access data. However POST calls are failing only in production because the api is hosted with https://, in local environment it works (htt://localhost:8000).
I am providing a correct CSRF token in the header.
It seems that the error is happening here: https://github.com/toastdriven/django-tastypie/blob/master/tastypie/authentication.py#L259 where the api require a Referer header to check that the call is secure.
It does not seem possible to set in my ajax headers the Referer value directly, only if the name of the header starts with X-.
Thanks in advance for any solutions or tips to solve this issue.
Maxime.
The reason for that check is explained in the original source of that method:
# Suppose user visits http://example.com/
# An active network attacker (man-in-the-middle, MITM) sends a
# POST form that targets https://example.com/detonate-bomb/ and
# submits it via JavaScript.
#
# The attacker will need to provide a CSRF cookie and token, but
# that's no problem for a MITM and the session-independent
# nonce we're using. So the MITM can circumvent the CSRF
# protection. This is true for any HTTP connection, but anyone
# using HTTPS expects better! For this reason, for
# https://example.com/ we need additional protection that treats
# http://example.com/ as completely untrusted. Under HTTPS,
# Barth et al. found that the Referer header is missing for
# same-domain requests in only about 0.2% of cases or less, so
# we can use strict Referer checking.
So, you may or may not benefit from the referrer check - it's up to you to decide.
If you wish to override it, just set your models to authenticate using a subclass of SessionAuthentication, and override the is_authenticated(self, request, **kwargs) function to your needs. The original method is quite succinct, so honestly I'd just copy-paste it and remove the offending if request.is_secure(): block rather than tricking the superclass into thinking the request has a referrer.
You can also skip checking the referer in AJAX calls by making the request object think it was not made by a secure call. That way, you keep your CSRF checks and everything else, just skip the referer. Here's a sample middleware:
class UnsecureAJAX(object):
def process_request(self, request):
setattr(request, '_is_secure_default', request._is_secure)
def _is_secure():
if request.is_ajax():
return False
else:
return request._is_secure_default()
setattr(request, '_is_secure', _is_secure)
It is also now technically possible to change the Referer part of a request with webRequest API.
You would need an onBeforeSendHeaders listener with ["blocking", "requestHeaders"] options, and corresponding permissions. Then you can intercept your own requests and modify the header.
Related
I am new to Django and I have 2 endpoints in a REST API:
api.myapp.com/PUBLIC/
api.myapp.com/PRIVATE/
As suggested by the names, the /PUBLIC endpoint is open to anyone. However, I want the /PRIVATE endpoint to only accept calls from myapp.com (my frontend). How can I do this? Thanks!
Without knowing how you apps and servers are setup, I think you can solve this problem with the django-cors-headers package. I skimmed the entire read me, and the signals at the bottom looks like a solution to your problem. This part here:
A common use case for the signal is to allow all origins to access a
subset of URL's, whilst allowing a normal set of origins to access all
URL's. This isn't possible using just the normal configuration, but it
can be achieved with a signal handler.
First set CORS_ALLOWED_ORIGINS to the list of trusted origins that are
allowed to access every URL, and then add a handler to
check_request_enabled to allow CORS regardless of the origin for the
unrestricted URL's. For example:
# myapp/handlers.py from corsheaders.signals import check_request_enabled
def cors_allow_api_to_everyone(sender, request, **kwargs):
return request.path.startswith('/PUBLIC/')
check_request_enabled.connect(cors_allow_api_to_everyone)
So for most cases django-cors-headers are set as an option for the entire project, but there seems to be a way here for you to allow a subset of your api (/PUBLIC in your case) to be allowed for everyone, but the rest is private.
So your config would be
CORS_ALLOWED_ORIGINS = [
"https://myapp.com",
]
That allows myapp.com to reach everything.
cors_allow_api_to_everyone is a function checking for a truth value.
If it is true, the request is allowed.
check_request_enabled.connect(cors_allow_api_to_everyone) connects your truth-check-function to the django-cors-headers signal.
I'm developing multitenant application using django. Every things works fine. But in the case of ALLOWED_HOST , I've little bit confusion, that how to manage dynamic domain name. I know i can use * for any numbers for domain, But i didn't wan't to use * in allowed host.
Here is my question is there any way to manage allowed host dynamically.
According to the Django doc,
Values in this list can be fully qualified names (e.g. 'www.example.com'), in which case they will be matched against the request’s Host header exactly (case-insensitive, not including port). A value beginning with a period can be used as a subdomain wildcard: '.example.com' will match example.com, www.example.com, and any other subdomain of example.com. A value of '*' will match anything; in this case you are responsible to provide your own validation of the Host header (perhaps in a middleware; if so this middleware must be listed first in MIDDLEWARE).
So, if you want to match a certain subdomain, you can use the subdomain wildcard as explained above. If you want a different validation, as the documentation says, the right way to do it would be to allow all hosts and write your own middleware.
If you don't know about middlewares, they are a simple mechanism to add functionality when certain events occur. For example, you can create a middleware function that executes whenever you get a request. This would be the right place for you to put your Host validation. You can get the host using the request object. Refer to the official docs on middleware if you want to know more.
A simple middleware class for your problem could look something like -
import requests
from django.http import HttpResponse
class HostValidationMiddleware(object):
def process_view(self, request, view_func, *args, **kwargs):
host = request.get_host()
is_host_valid = # Perform host validation
if is_host_valid:
# Django will continue as usual
return None
else:
response = HttpResponse
response.status_code = 403
return response
If you need more information, comment below and I will try to help.
The setting should be loaded once on startup - generally those settings don't change in a production server setup. You can allow multiple hosts, or allow multiple subdomains.
*.example.com would work, or you can pass a list of domains if I remember correctly.
Our DjangoCMS site is accessible via http and https.
Anonymous usage via http is ok. But I want to disable logins via http.
Is there a way to force the usage of https as soon as the user wants to login?
Even the login-page (with username and password fields) should not be available via http.
Background: I don't want the password to go over the wire unencrypted.
Update: The site gets hosted on an apache web server.
As I already mentioned it in the comments I strongly suggest you to NOT only serve the login page via https.
Doing so just hides the fact that for example session information and authentication data is still transfered on the other requests unencrypted via http. Your site will not be secure at all.
You're just pseudo-securing stuff so it's fancy to somebodys eye. It's just like using the password 12345.
So please serve your website over https to the user. A small guide, for nginx or apache2, on how to redirect your traffic from http to https can be found here:
Redirecting to SSL using nginx
Force redirect to SSL for all pages apart from one
As you've accepted in the comments, pushing all traffic to HTTPS is the best solution these days. If you only authenticate via SSL, you'll need to consider encryption/security in everything you write moving forward rather than it just being the default.
So in your server config, force all traffic to port 443. Assuming you're using apache you'd do this;
<VirtualHost *:80>
ServerName www.example.com
Redirect / https://www.example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName www.example.com
# ... SSL configuration goes here
</VirtualHost>
Then in your Django settings turn on secure cookies;
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
As a side note, Django has a setting to redirect HTTP traffic to HTTPS which is SECURE_SSL_REDIRECT, but if you're doing this at the apache/nginx level you don't need to worry. For some further reading on SSL/HTTPS have a look here; https://docs.djangoproject.com/en/1.11/topics/security/#ssl-https
Django request has a is_secure()
You can check it in the view and redirect if not is secure:
if not request.is_secure():
return redirect("https://www.yourdomain.com/login")
#user1 has the logic right and you should serve your site through https in order to have it safe.
But in order to answer your exact question:
As shown here: How to know using Django if server is secure (uses https) and as #Zartch mentions in his answer, you should use the is_secure() HTTPRequest method to check if your request is coming through https.
In order to use is_secure() you need to check an incoming request
to your views:
def my_login_view(request):
if request.is_secure():
Do loggin
else:
Don't login
Now you can protect your other views with the login_required decorator:
#login_required(login_url='/your/login/url')
def protected_view(request):
...
Personal suggestion: I use Django Rest Framework extensively and I would suggest it, in your case, because it has an isAuthenticatedOrReadOnly permission class which you will like:
class MyLoginView(ObtainAuthToken):
"""
Login view for token-based authentication
"""
def post(self, request, *args, **kwargs):
if request.is_secure():
super().post(request, *args, **kwargs)
else:
Probably some redirect goes here...
Then in any other view or class-based view:
class MyOtherView(generics.GenericAPIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
...
The above will ensure that your users can login only through https and if they are not logged in they will only see a read-only view.
Give it a try if you like.
You didn't accept an answer yet, so I thought, that the answers might not match your question well enough.
Anonymous usage via http is ok. But I want to disable logins via http.
As I understand, you have the following use case:
Some person visits your website as anonymous user, the used protocol is http.
This person opens the login form and logs in, the form is sent through https.
The logged in user continues browsing your website through https.
If the login form is routed to the secure version your user will continue browsing your website in https mode (as long as all other links are agnostic to the used protocol).
<form action="https://your.domain/path/to/login/" method="post">
If the application you're using to authenticate users is sanely written, you can configure your settings.LOGIN_URL, else you'll probably need to do it in the template form.
I recommend allowing session cookies to be sent only through https, using settings.SESSION_COOKIE_SECURE.
With this setting, the user will still be able to make http requests, but the session cookie will not be sent (and the user will be treated as an anonymous user).
Please, make sure that your login view accepts only secure post requests (or else the user may send the password through non https protocol).
This can be achieved either in the Django code (like John Moutafis suggests with his MyLoginView) or at production server level by rejecting non https post requests for the login URL.
--
Could you provide access to the repository with the code?
You may use one or a combination of:
htaccess file, take a look here
webserver level, depend of server type: Apache, Nginx, IIS ?...
django server level, take a look here, and host header validation or SSL/HTTPS
I see 'Sorry, not implemented yet. Please append "?format=json" to
your URL.'. I need always append string "?format=json". Can I make a
output in JSON by default?
Regards,
Vitaliy
From the tastypie cookbook, in order to change the default format, you need to override the determine_format() method on your ModelResource:
class MyResource(ModelResource):
....
def determine_format(self, request):
return 'application/json'
The above link demonstrates alternative methods of determining output format.
Also, I don't think a valid answer is essentially "You don't need this".
Edit
It appears GregM's answer is probably (I haven't tested it) the most correct with the new version of TastyPie, as per documentation putting the following in your settings.py will restrict the serialization output to json.
TASTYPIE_DEFAULT_FORMATS = ['json']
As of tastypie 0.9.13, if you do not need XML support you can disable it globally by setting TASTYPIE_DEFAULT_FORMATS to just ['json'] in your settings.py file. Requests should then default to JSON.
I've tested setting TASTYPIE_DEFAULT_FORMATS to ['json'] but it doesn't prevent the "Sorry not implemented yet" message when viewing the API from a browser.
I am able to make that warning go away by forcing the "Accept" header to 'application/json' in a middleware:
class TastyJSONMiddleware(object):
"""
A Django middleware to make the Tastypie API always output in JSON format
instead of telling browsers that they haven't yet implemented text/html or
whatever.
WARNING: This includes a hardcoded url path for /api/. This is not 'DRY'
because it means you have to edit two places if you ever move your API
path.
"""
api_prefix = '/api/'
def process_request(self, request):
if request.path.startswith(self.api_prefix):
request.META['HTTP_ACCEPT'] = 'application/json'
Tasytpie has the defaults set as 'application/json'. But that is overridden by Browser request.
According to Tastypie, the default is overridden by Request Header ACCEPT and your format specification in GET ie. ?format=json. When you send request from browsers, if you see the Request HTTP Header sent, its something like -
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
The application/xml overrides the default in Tastypie Resource. Therefore, either you can set Browser Header to have 'application/json' (Bad idea) or you just specify in GET.
If you hit the same API url using CURL, you will see the JSON output without specifying that in GET.
To examine/test your REST API, use a Rest client instead of a browser, preferably one that knows how to pretty print JSON. I use the Postman plugin for Google Chrome.
If you want pretty json in command line:
curl https://api.twitter.com/1.1/search/tweets.json | python -m json.tool
My client-server application is mainly based on special purpose http server that communicates with client in an Ajax like fashion, ie. the client GUI is refreshed upon asynchronous http request/response cycles.
Evolvability of the special purpose http server is limited and as the application grows, more and more standard features are needed which are provided by Django for instance.
Hence, I would like to add a Django application as a facade/reverse-proxy in order to hide the non-standard special purpose server and be able to gain from Django. I would like to have the Django app as a gateway and not use http-redirect for security reasons and to hide complexity.
However, my concern is that tunneling the traffic through Django on the serer might spoil performance. Is this a valid concern?
Would there be an alternative solution to the problem?
Usually in production, you are hosting Django behind a web container like Apache httpd or nginx. These have modules designed for proxying requests (e.g. proxy_pass for a location in nginx). They give you some extras out of the box like caching if you need it. Compared with proxying through a Django application's request pipeline this may save you development time while delivering better performance. However, you sacrifice the power to completely manipulate the request or proxied response when you use a solution like this.
For local testing with ./manage.py runserver, I add a url pattern via urls.py in an if settings.DEBUG: ... section. Here's the view function code I use, which supports GET, PUT, and POST using the requests Python library: https://gist.github.com/JustinTArthur/5710254
I went ahead and built a simple prototype. It was relatively simple, I just had to set up a view that maps all URLs I want to redirect. The view function looks something like this:
def redirect(request):
url = "http://%s%s" % (server, request.path)
# add get parameters
if request.GET:
url += '?' + urlencode(request.GET)
# add headers of the incoming request
# see https://docs.djangoproject.com/en/1.7/ref/request-response/#django.http.HttpRequest.META for details about the request.META dict
def convert(s):
s = s.replace('HTTP_','',1)
s = s.replace('_','-')
return s
request_headers = dict((convert(k),v) for k,v in request.META.iteritems() if k.startswith('HTTP_'))
# add content-type and and content-length
request_headers['CONTENT-TYPE'] = request.META.get('CONTENT_TYPE', '')
request_headers['CONTENT-LENGTH'] = request.META.get('CONTENT_LENGTH', '')
# get original request payload
if request.method == "GET":
data = None
else:
data = request.raw_post_data
downstream_request = urllib2.Request(url, data, headers=request_headers)
page = urllib2.urlopen(downstream_request)
response = django.http.HttpResponse(page)
return response
So it is actually quite simple and the performance is good enough, in particular if the redirect goes to the loopback interface on the same host.