從原始碼角度理解Django中給特定檢視函式放開CSRF校驗的原理
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
由上圖原始碼可知:如果request物件有_dont_enforce_csrf_checks 屬性,且為True,則接受此次請求,相當於不進行csrf 校驗
3)檢視函式有csrf_exempt 屬性,且為True
檢視函式有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 裝飾器 必須放在最上面,我們知道Django中的裝飾器的順序是從底至上,如果不放在最上面,這繞過CSRF校驗機制失敗,原因在0X02中會解釋
我們再來看一下Django CSRF中介軟體的程式碼,在process_view 函式中會對請求進行CSRF 校驗
這裡callback 指的是檢視函式
注意第二個紅框,意為:如果檢視函式有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