CSRF
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。
摘自百科:攻击通过在授权用户访问的页面中包含链接或者脚本的方式工作。
例 如:一个网站用户Bob可能正在浏览聊天论坛,而同时另一个用户Alice也在此论坛中,并且后者刚刚发布了一个具有Bob银行链接的图片消息。设想一下,Alice编写了一个在Bob的银行站点上进行取款的form提交的链接,并将此链接作为图片src。如果Bob的银行在cookie中保存他的授权信息,并且此cookie没有过期,那么当Bob的浏览器尝试装载图片时将提交这个取款form和他的cookie,这样在没经Bob同意的情况下便授权了这次事务。 CSRF是一种依赖web浏览器的、被混淆过的代理人攻击(deputy attack)。在上面银行示例中的代理人是Bob的web浏览器,它被混淆后误将Bob的授权直接交给了Alice使用。
具体的细节和危害可参考百科
- https://en.wikipedia.org/wiki/Cross-site_request_forgery
- https://baike.baidu.com/item/CSRF
- https://docs.djangoproject.com/en/dev/ref/csrf/
django & csrf
django CSRF保护机制原理
django通过token的校验来保证请求来自同一用户。django 在第一次收到某个客户端的请求时,会在服务器端随机生成一个 token ,并且把这个 token 保存在 cookie 里。后面每次 POST 请求都会带上这个 token,django在后端验证token是否一致,这样就能避免被 CSRF 攻击。 基本流程如下:
- 返回Http Response相应时,django会在后端随机生成一个csrftoken,放在响应response的cookie里。
- 用户发起表单 POST请求时,表单里必须包含一个 csrfmiddlewaretoken 字段 ,该字段不需要用户计算,可以直接用tag完成,具体见下面。
- 在处理 POST 请求之前,django 会验证这个提交的表单里的 csrfmiddlewaretoken 字段的值和请求的 cookie 里的 csrftoken 字段的值和是否一致。如果一致,那么接收这个请求,否则,这个可能是一个来自csrf 攻击的非法请求,返回 403 Forbidden. 早期的版本里,csrftoken和csrfmiddlewaretoken是相等的,但是最新的版本里,这两个值中间经过了一定的算法转发,需要通过算法去匹配是否一致。
- 如果是 ajax POST 请求,可以添加一个 X-CSRFTOKEN header,其值为 cookie 里的 csrftoken 的值
Django如何使用CSRF
- CSRF中间件在
MIDDLEWARE
设置里是默认激活的,如果你要重写设置文件,记得把'django.middleware.csrf.CsrfViewMiddleware'
放在你要处理CSRF攻击的middleware前面。如果你想去掉这个middleware(不推荐),记得在你想包含的函数上使用csrf_protect()
- 在所有的POST表单里,如果是内部URL,在<form>里添加
csrf_token
tag,它会帮忙生成csrfmiddlewaretoken
字段,这个是隐藏字段。如果是外部URL,不要使用这种方法,因为它会导致token泄露,具体解决方法有专门章节讲解。 - 在相应的函数视图里,使用
RequestContext
去渲染模板响应。RequestContext
会处理csrf_token
这个 tag, 从而自动为表单添加一个名为csrfmiddlewaretoken
的 input。如果你用render()
函数,generic view或者contrib app,它们已默认使用RequestContext
,不需要特殊处理。
AJAX POST
上一节介绍了CSRF使用的基本方法。对于AJAX请求,处理上会比较复杂一些。详细可参考https://docs.djangoproject.com/en/2.0/ref/csrf/
post方式不同于get方式可以被django直接得到,因为django为post加入了csrf保护, 详细的文档地址https://docs.djangoproject.com/en/dev/ref/csrf/
添加middleware
注意: 在最新版本中,在setting.py里'django.middleware.csrf.CsrfViewMiddleware'
,默认是使用中的,如果没有请自行添加,并且确保此引用在其他所有viewware前面
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',# this line is vsrf
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_cas.middleware.CASMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
在ajax方法触发前加入一段js
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
实现ajax的beforeSend
函数,头里面添加添加X-CSRFToken
,值为csrftoken
$.ajax({
url: '',
data: {
storage_location: storage_location,
},
dataType : 'json',
type : 'post',
beforeSend: function (xhr, settings) {
var csrftoken = getCookie('csrftoken');
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
// CODE
},
success: function (data) {
// CODE
console.log(data);
},
complete: function(){
// console.log("complete");
},
error: function (jqXHR, textStatus, errorThrown) {
// CODE
console.log("error");
}
});
视图中的处理举例,摘自网上
view.py:
def ticket_handler(request):
if request.method == 'POST':
if request.GET['action'] == 'get_vmlist':
try:
d = {}
env = request.POST['env']
print env # 这里就可以看到env的值被正确传递给后台了
d['Result'] = 'Fail'
s = json.dumps(d)
return HttpResponse(s)
避开CSRF检查
在 settings.py 中 MIDDLEWARE_CLASSES 中 注释掉
'django.middleware.csrf.CsrfViewMiddleware'
在需要在views.py里要出发post请求的函数前加入
@csrf_exempt
,之前要引入这个装饰器
from django.views.decorators.csrf import csrf_exempt
单独CSRF检查
然后,显式针对单个view函数进行csrf保护
from django.views.decorators.csrf import csrf_protect
@csrf_protect
def my_view(request):
c = {}
# ...
return render(request, "a_template.html", c)
代码走读
下面是两个基本调用函数,用户设置和获取token函数
Token存储在http消息的cookie里,token值的读写是围绕着个cookie进行的。
CSRF_COOKIE_NAME = 'csrftoken'
def _get_token(self, request):
if settings.CSRF_USE_SESSIONS:
try:
return request.session.get(CSRF_SESSION_KEY)
except AttributeError:
raise ImproperlyConfigured(
'CSRF_USE_SESSIONS is enabled, but request.session is not '
'set. SessionMiddleware must appear before CsrfViewMiddleware '
'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
)
else:
try:
cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
except KeyError:
return None
csrf_token = _sanitize_token(cookie_token)
if csrf_token != cookie_token:
# Cookie token needed to be replaced;
# the cookie needs to be reset.
request.csrf_cookie_needs_reset = True
return csrf_token
def _set_token(self, request, response):
if settings.CSRF_USE_SESSIONS:
request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
else:
response.set_cookie(
settings.CSRF_COOKIE_NAME,
request.META['CSRF_COOKIE'],
max_age=settings.CSRF_COOKIE_AGE,
domain=settings.CSRF_COOKIE_DOMAIN,
path=settings.CSRF_COOKIE_PATH,
secure=settings.CSRF_COOKIE_SECURE,
httponly=settings.CSRF_COOKIE_HTTPONLY,
)
# Set the Vary header since content varies with the CSRF cookie.
patch_vary_headers(response, ('Cookie',))
_get_token
函数里,从request.COOKIES[settings.CSRF_COOKIE_NAME]
里读取cookie_token,然后通过函数_sanitize_token
处理一下
_set_token
函数里,通过response.set_cookie
设置cookie
生成csrftoken
在response函数里,设置token到cookie里,并设置 csrf_cookie_set = True
,cookie的名字为CSRF_COOKIE
def process_response(self, request, response):
if not getattr(request, 'csrf_cookie_needs_reset', False):
if getattr(response, 'csrf_cookie_set', False):
return response
if not request.META.get("CSRF_COOKIE_USED", False):
return response
# Set the CSRF cookie even if it's already set, so we renew
# the expiry timer.
self._set_token(request, response)
response.csrf_cookie_set = True
return response
生成csrfmiddlewaretoken
没找到代码
渲染csrfmiddlewaretoken
django.template.defaulttag.py
@register.tag
def csrf_token(parser, token):
return CsrfTokenNode()
class CsrfTokenNode(Node):
def render(self, context):
csrf_token = context.get('csrf_token')
if csrf_token:
if csrf_token == 'NOTPROVIDED':
return format_html("")
else:
return format_html("<input type='hidden' name='csrfmiddlewaretoken' value='{}' />", csrf_token)
else:
# It's very probable that the token is missing because of
# misconfiguration, so we raise a warning
if settings.DEBUG:
warnings.warn(
"A {% csrf_token %} was used in a template, but the context "
"did not provide the value. This is usually caused by not "
"using RequestContext."
)
return ''
token这个值生成和传递进来的地方没有找到。
在django 1.9,这个值跟csrftoken相等;在djagno 1.11,这个值已经不一样了,应该是经过加密。
验证token
django.middleware.csrf.py
在请求处理函数里读取token,并做比较。
该函数的逻辑在前面讲django原理时已讲过,它会都去cookie里的csrftoken值和POST请求里的csrfmiddlewaretoken值,并对这两个值进行匹配比较。
def process_view(self, request, callback, callback_args, callback_kwargs):
if getattr(request, 'csrf_processing_done', False):
return None
csrf_token = self._get_token(request)
if csrf_token is not None:
# Use same token next time.
request.META['CSRF_COOKIE'] = csrf_token
# Wait until request.META["CSRF_COOKIE"] has been manipulated before
# bailing out, so that get_token still works
if getattr(callback, 'csrf_exempt', False):
return None
# Assume that anything not defined as 'safe' by RFC7231 needs protection
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
if getattr(request, '_dont_enforce_csrf_checks', False):
# Mechanism to turn off CSRF checks for test suite.
# It comes after the creation of CSRF cookies, so that
# everything else continues to work exactly the same
# (e.g. cookies are sent, etc.), but before any
# branches that call reject().
return self._accept(request)
if request.is_secure():
# 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
# secret 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.
referer = force_text(
request.META.get('HTTP_REFERER'),
strings_only=True,
errors='replace'
)
if referer is None:
return self._reject(request, REASON_NO_REFERER)
referer = urlparse(referer)
# Make sure we have a valid URL for Referer.
if '' in (referer.scheme, referer.netloc):
return self._reject(request, REASON_MALFORMED_REFERER)
# Ensure that our Referer is also secure.
if referer.scheme != 'https':
return self._reject(request, REASON_INSECURE_REFERER)
# If there isn't a CSRF_COOKIE_DOMAIN, require an exact match
# match on host:port. If not, obey the cookie rules (or those
# for the session cookie, if CSRF_USE_SESSIONS).
good_referer = (
settings.SESSION_COOKIE_DOMAIN
if settings.CSRF_USE_SESSIONS
else settings.CSRF_COOKIE_DOMAIN
)
if good_referer is not None:
server_port = request.get_port()
if server_port not in ('443', '80'):
good_referer = '%s:%s' % (good_referer, server_port)
else:
# request.get_host() includes the port.
good_referer = request.get_host()
# Here we generate a list of all acceptable HTTP referers,
# including the current host since that has been validated
# upstream.
good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
good_hosts.append(good_referer)
if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
reason = REASON_BAD_REFERER % referer.geturl()
return self._reject(request, reason)
if csrf_token is None:
# No CSRF cookie. For POST requests, we insist on a CSRF cookie,
# and in this way we can avoid all CSRF attacks, including login
# CSRF.
return self._reject(request, REASON_NO_CSRF_COOKIE)
# Check non-cookie token for match.
request_csrf_token = ""
if request.method == "POST":
try:
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
except IOError:
# Handle a broken connection before we've completed reading
# the POST data. process_view shouldn't raise any
# exceptions, so we'll ignore and serve the user a 403
# (assuming they're still listening, which they probably
# aren't because of the error).
pass
if request_csrf_token == "":
# Fall back to X-CSRFToken, to make things easier for AJAX,
# and possible for PUT/DELETE.
request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')
request_csrf_token = _sanitize_token(request_csrf_token)
if not _compare_salted_tokens(request_csrf_token, csrf_token):
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
通过_compare_salted_tokens
函数对两个token进行比较
django 1.9和djagno1.11在处理上有一些变化,下面列了变化的部分,我没有仔细去查阅到底哪个版本开始改的,这是我用的最多的两个版本。
Django 1.9
class CsrfViewMiddleware(object):
def process_view(self, request, callback, callback_args, callback_kwargs):
try:
csrf_token = _sanitize_token(
request.COOKIES[settings.CSRF_COOKIE_NAME])
# Use same token next time
request.META['CSRF_COOKIE'] = csrf_token
except KeyError:
csrf_token = None
……
if not constant_time_compare(request_csrf_token, csrf_token):
return self._reject(request, REASON_BAD_TOKEN)
return self._accept(request)
django 1.11
class CsrfViewMiddleware(object):
......
csrf_token = self._get_token(request)
if csrf_token is not None:
# Use same token next time.
request.META['CSRF_COOKIE'] = csrf_token
……
request_csrf_token = _sanitize_token(request_csrf_token)
if not _compare_salted_tokens(request_csrf_token, csrf_token):
return self._reject(request, REASON_BAD_TOKEN)
比较一下他们的处理逻辑:
Django 1.9
(sanitize) csrf_token -> constant_time_compare <- request_csrf_token (csrfmiddlewaretoken)
django 1.11
(sanitize) csrf_token (_unsalt_cipher_token) -> constant_time_compare <- (sanitize) request_csrf_token (csrfmiddlewaretoken) (_unsalt_cipher_token)
可以看到,在django 1.11里有几个变化
- csrf_token的获取方法上,封装成了_get_token函数,对本例来说,本质上最后调用sanitize_token函数,这个是微小变化
- 比较之前,request_csrf_token在比较之前也用sanitize_token函数做了一下处理
- 比较时,不是内容直接比较,而是调用_unsalt_cipher_token函数做一下处理(我猜测是加解密相关处理),然后再进行比较。这儿特别要注意:在django 1.11 版本里,csrftoken和csrfmiddlewaretoken的值不一样了。
下面是比较函数,unsalt_cipher_token是新加的。
def _compare_salted_tokens(request_csrf_token, csrf_token):
# Assume both arguments are sanitized -- that is, strings of
# length CSRF_TOKEN_LENGTH, all CSRF_ALLOWED_CHARS.
return constant_time_compare(
_unsalt_cipher_token(request_csrf_token),
_unsalt_cipher_token(csrf_token),
)