从源码角度理解Django中给特定视图函数放开CSRF校验的原理

2018-10-08 9,873

0x00. 引言

公司内部自动化代码审计系统需要将每次审计结果推送至堡垒机存储,最开始,每次推送都失败,后来查明是因为触发了堡垒机的CSRF校验机制。

所以希望对这个推送接口关闭CSRF校验,最后发现可以使用csrf_exempt这个Django自带的装饰器函数满足需求,处于好奇心,研究了csrf_exempt的实现原理,于是有了本文。


0x01. 可以实现对视图放开CSRF校验的3种方式

通过分析Django CSRF中间件源码可知:如果想对某一个视图放开CSRF校验,有3种方式


1)干掉CSRF中间件


2)在CSRF中间件生效之前,使得request对象有_dont_enforce_csrf_checks 属性,且为True


1-2.png


由上图源码可知:如果request对象有_dont_enforce_csrf_checks 属性,且为True,则接受此次请求,相当于不进行csrf 校验


3)视图函数有csrf_exempt 属性,且为True

1-1.png



视图函数有csrf_exempt 属性,且为True,则返回None,相当于放弃CSRF 校验


1)和 2) 都是基于全局的,这样以来,所有的视图都会放弃CSRF校验


只有3)可以自由的决定哪一个视图应该放弃CSRF校验,只要这个视图含有csrf_exempt 属性,且为True,则放弃CSRF校验,怎么样才能使得视图含有csrf_exempt 属性,且为True。


很简单有,两种方法,一种是手动添加,给每一个视图函数增加一个csrf_exempt 属性,且为True

另外一个是在每一个需要放弃CSRF校验的视图上加上一个csrf_exempt的属性,显然第二种方式更优雅,也符合解耦原则


那么下面我们就开始分析下Django 自带的装饰器函数 csrf_exempt 的实现原理


0x02. csrf_exempt 装饰器函数实现原理


from django.views.decorators.csrf import csrf_exempt


只需要在需要放开CSRF校验的视图上使用这个装饰器即可绕过CSRF校验机制


csrf_exempt.jpg


注意: 这里需要注意的是,csrf_exempt 装饰器 必须放在最上面,我们知道Django中的装饰器的顺序是从底至上,如果不放在最上面,这绕过CSRF校验机制失败,原因在0X02中会解释



我们再来看一下Django CSRF中间件的代码,在process_view 函数中会对请求进行CSRF 校验


这里callback 指的是视图函数


csrf_exempt_reason.jpg



注意第二个红框,意为:如果视图函数有csrf_exempt属性,且值为True,则实现绕过csrf校验


好了,我们在看一下csrf_exempt 装饰器函数代码:

from functools import wraps
from django.utils.decorators import available_attrs

def csrf_exempt(view_func):
    """
    Marks a view function as being exempt from the CSRF view protection.
    """

    # We could just do view_func.csrf_exempt = True, but decorators
    # are nicer if they don't have side-effects, so we return a new
    # function.
    def wrapped_view(*args, **kwargs):
        return view_func(*args, **kwargs)

    wrapped_view.csrf_exempt = True

    return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)


首先跟一下functools 中的wraps 

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

       Returns a decorator that invokes update_wrapper() with the decorated
       function as the wrapper argument and the arguments to wraps() as the
       remaining arguments. Default arguments are as for update_wrapper().
       This is a convenience function to simplify applying partial() to
       update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)


也就是说:wraps(view_func, assigned=available_attrs(view_func)) 返回的是一个偏函数,函数为:update_wrapper,默认参数为wrapped=视图函数,assigned=视图函数的属性,updated=要更新的属性,默认为WRAPPER_UPDATES = ('dict',)


要理解  wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) 返回的是啥,则需要进一步跟一下update_wrapper:

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):
    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper


update_wrapper 参数中的wrapper即为csrf_exempt装饰器函数 中的wrapped_view函数


for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))

    将视图函数中的属性拷贝至wrapped_view 函数中

最后返回wrapped_view 函数


在csrf_exempt 装饰器函数中有这么一行代码:


wrapped_view.csrf_exempt = True


也就是给wrapped_view加了一个属性csrf_exempt,且值为True


从而绕过了CSRF 校验。从源码角度理解Django 中给特定视图函数放开CSRF校验的原理 https://www.secpulse.com/archives/76204.html


经过装饰器装饰的视图函数最后返回的也是一个函数,如果csrf_exempt装饰器函数不放在最上面,则经过装饰器返回的函数中则无csrf_exempt 属性,导致绕过CSRF 校验失败


不行我们来做个测试:


测试代码:

#! -*- coding:utf8 -*-
from functools import wraps
from django.utils.decorators import available_attrs


def is_valid_json(func):
    """
    检测post的数据是否为合法json
    不带参装饰器
    :return:
    """

    def wrapper(*args, **kwargs):
        print "is_valid_json"
        return func(*args, **kwargs)

    return wrapper


def csrf_exempt(view_func):
    """
    Marks a view function as being exempt from the CSRF view protection.
    """

    # We could just do view_func.csrf_exempt = True, but decorators
    # are nicer if they don't have side-effects, so we return a new
    # function.
    def wrapped_view(*args, **kwargs):
        return view_func(*args, **kwargs)

    wrapped_view.csrf_exempt = True
    return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view)


@is_valid_json
@csrf_exempt
def test():
    print "test"


if __name__ == '__main__':
    print getattr(test, 'csrf_exempt', False)


csrf_exempt 装饰器放在最上面


结果为True


csrf_exempt 装饰器不放在最上面=


结果为False


0x03. 总结


为了搞明白csrf_exempt这个装饰器函数的原理,分析了Django的CSRF 中间件实现原理,加深了对Django安全机制的理解,

对以后的安全开发、安全培训(比如给Python开发的同学讲解Django的安全机制)、代码审计都很有好处,

Python的安全愈发引起大家的注意,也希望更多的同学加入到Python安全的分享当中来。

从源码角度理解Django 中给特定视图函数放开CSRF校验的原理 https://www.secpulse.com/archives/76204.html



本文作者:hOlKSOMX

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/76204.html

Tags:
评论  (0)
快来写下你的想法吧!

hOlKSOMX

文章数:5 积分: 69

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号