Tornado源碼分析 --- Cookie和XSRF機制

Cookie和Session的理解:

具體Cookie的介紹,能夠參考:HTTP Cookie詳解html

能夠先查看以前的一篇文章:Tornado的Cookie過時問題python

XSRF跨域請求僞造(Cross-Site-Request-Forgery):

簡單的說,是攻擊者經過一些技術手段欺騙用戶的瀏覽器去訪問一個本身曾經認證過的網站並執行一些操做(如發郵件,發消息,甚至財產操做如轉帳和購買商品)。因爲瀏覽器曾經認證過,因此被訪問的網站會認爲是真正的用戶操做而去執行。這利用了web中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求發自某個用戶的瀏覽器,卻不能保證請求自己是用戶自願發出的。web

詳細細節請參考:XSRF介紹正則表達式

 

由於Tornado中XSRF機制的實現是基於Cookie的(XSRF驗證信息是保存在Cookie中),因此咱們先來分析Tornado中Cookie源碼的實現。跨域

Tornado中Cookie源碼分析:

set_secure_cookie模塊:

用法介紹:瀏覽器

  set_secure_cookie方法是對set_cookie方法的包裝。要使用該方法,必須在 Application 中的 settings中指定"cookie_secret"(應該是一個通過HMAC加密的夠長且隨機的字節序列),若是想讀取這個cookie設置,能夠經過get_secure_cookie方法(後文會進行介紹)。安全

參數介紹:服務器

  expires_days:設置cookie在瀏覽器端的有效期,經過源碼能夠知道,其默認爲30天。注意,其該參數,跟get_secure_cookie方法中的」max_age_days「沒有必然的聯繫,可使用一個小於」expires_days「的」max_age_days「在服務端來控制安全Cookie的有效期。websocket

  version:該參數主要是爲了兼容舊的簽名方式,版本號1使用SHA1簽名,版本2使用SHA256簽名;對於不一樣的版本,在後文get_secure_cookie方法中,解析Cookie的時候對應着不一樣的版本解析方法,最新版本的Tornado默認是使用SHA156簽名的版本2。cookie

  name,value:這個則是相應的Cookie名稱和對應的Cookie的值。

源碼:

 1 def set_secure_cookie(self, name, value, expires_days=30, version=None,**kwargs):
 2     self.set_cookie(name, self.create_signed_value(name, value,version=version),
 3                     expires_days=expires_days, **kwargs)
 4 
 5 def create_signed_value(self, name, value, version=None):
 6     self.require_setting("cookie_secret", "secure cookies")
 7     secret = self.application.settings["cookie_secret"]
 8     key_version = None
 9     if isinstance(secret, dict):
10         if self.application.settings.get("key_version") is None:
11             raise Exception("key_version setting must be used for secret_key dicts")
12         key_version = self.application.settings["key_version"]
13     return create_signed_value(secret, name, value, version=version,key_version=key_version)

  開始直接調用 self.require_setting("cookie_secret", "secure cookies") 來判斷是否設置了簽名密鑰。

  以後就經過 create_signed_value() 方法對不一樣的cookie進行不一樣的簽名方式:

 1 def create_signed_value(secret, name, value, version=None, clock=None,
 2                         key_version=None):
 3     if version is None:
 4         version = DEFAULT_SIGNED_VALUE_VERSION
 5     if clock is None:
 6         clock = time.time
 7 
 8     timestamp = utf8(str(int(clock())))
 9     value = base64.b64encode(utf8(value))
10     if version == 1:
11         signature = _create_signature_v1(secret, name, value, timestamp)
12         value = b"|".join([value, timestamp, signature])
13         return value
14     elif version == 2:
15         def format_field(s):
16             return utf8("%d:" % len(s)) + utf8(s)
17         to_sign = b"|".join([
18             b"2",
19             format_field(str(key_version or 0)),
20             format_field(timestamp),
21             format_field(name),
22             format_field(value),
23             b''])
24 
25         if isinstance(secret, dict):
26             assert key_version is not None, 'Key version must be set when sign key dict is used'
27             assert version >= 2, 'Version must be at least 2 for key version support'
28             secret = secret[key_version]
29 
30         signature = _create_signature_v2(secret, to_sign)
31         return to_sign + signature
32     else:
33         raise ValueError("Unsupported version %d" % version)

Cookie 值經過 value = base64.b64encode(utf8(value)) 進行 base64 編碼轉換,因此 set_secure_cookie 能支持任意的字符,這與 set_cookie 方法不一樣:

分析:

  看下set_cookie()的源碼(僅截取部分):

1 def set_cookie(self, name, value, domain=None, expires=None, path="/",
2                expires_days=None, **kwargs):
3     name = escape.native_str(name)
4     value = escape.native_str(value)
5     if re.search(r"[\x00-\x20]", name + value):
6         raise ValueError("Invalid cookie %r: %r" % (name, value))

  在escape模塊中找到對應的 native_str() 方法:escape.py

1 if str is unicode_type:
2     native_str = to_unicode
3 else:
4     native_str = utf8

  對於 unicode_type 的判斷,其定義在 util模塊中:util.py

1 bytes_type = bytes
2 if PY3:
3     unicode_type = str
4     basestring_type = str
5 else:
6     # The names unicode and basestring don't exist in py3 so silence flake8.
7     unicode_type = unicode  # noqa
8     basestring_type = basestring  # noqa

結論:

  python2 是轉換爲 str,python3 時轉換爲 unicode string,且不容許輸入 「\x00-\x20」 之間的字符,其實現代碼中由正則表達式來檢查。

 

接着回過頭看上面的源碼:

  version字段,默認是設置爲DEFAULT_SIGNED_VALUE_VERSION(在源碼中最開始定義了 DEFAULT_SIGNED_VALUE_VERSION = 2)。若是要指定版本,則須要在 set_secure_cookie() 方法中經過參數傳遞進來進行設置,咱們也能夠發現:

    • 對於版本1,version=1:簡單的 「value|timestamp|signature」 拼接
    • 對於版本2,version=2:其增長了幾個字段,而且返回記錄了字符串的長度,尤爲是預留的 key_version 字段爲後續輪流使用多個 cookie_secret 提供了支持。而且對整個字符串進行了加密處理,版本1僅僅加密了value。

  版本1簽名方式:使用的SHA1

1 def _create_signature_v1(secret, *parts):
2     hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
3     for part in parts:
4         hash.update(utf8(part))
5     return utf8(hash.hexdigest())

  版本2簽名方式:使用的SHA256

1 def _create_signature_v2(secret, s):
2     hash = hmac.new(utf8(secret), digestmod=hashlib.sha256)
3     hash.update(utf8(s))
4     return utf8(hash.hexdigest())

 

get_secure_cookie模塊:

  get_secure_cookie 方法簽名中的 value 參數指的是經過 set_secure_cookie 加密簽名後的 Cookie 值,默認是 None 則會從客戶端發送回來的 Cookies 中獲取指定名稱name的 Cookie 值做爲 value。而後再傳入 max_age_days, min_version等值進行Cookie的解碼驗證。

源碼:

1 def get_secure_cookie(self, name, value=None, max_age_days=31,
2                       min_version=None):
3     self.require_setting("cookie_secret", "secure cookies")
4     if value is None:
5         value = self.get_cookie(name)
6     return decode_signed_value(self.application.settings["cookie_secret"],
7                                name, value, max_age_days=max_age_days,
8                                min_version=min_version)

解碼驗證函數:decode_signed_value()

 1 def decode_signed_value(secret, name, value, max_age_days=31,
 2                         clock=None, min_version=None):
 3     if clock is None:
 4         clock = time.time
 5     if min_version is None:
 6         min_version = DEFAULT_SIGNED_VALUE_MIN_VERSION
 7     if min_version > 2:
 8         raise ValueError("Unsupported min_version %d" % min_version)
 9     if not value:
10         return None
11 
12     value = utf8(value)
13     version = _get_version(value)
14 
15     if version < min_version:
16         return None
17     if version == 1:
18         return _decode_signed_value_v1(secret, name, value,
19                                        max_age_days, clock)
20     elif version == 2:
21         return _decode_signed_value_v2(secret, name, value,
22                                        max_age_days, clock)
23     else:
24         return None

  默認的 min_version 爲 DEFAULT_SIGNED_VALUE_MIN_VERSION(在源碼中最開始定義了 DEFAULT_SIGNED_VALUE_MIN_VERSION = 1),對於舊版本(版本 1 )加密簽名的 cookie 數據中沒有版本號這個字段,默認取 1。而後與指定的 min_version 進行比較,僅當大於等於 min_version 才進行下一步驗證。版本 1 由函數 _decode_signed_value_v1 驗證,版本 2 由 函數 _decode_signed_value_v2 驗證,這兩個函數主要就是按照對應簽名格式解析數據,並對目標籤名和時間戳等字段進行比較驗證。

版本1解碼:

 1 def _decode_signed_value_v1(secret, name, value, max_age_days, clock):
 2     parts = utf8(value).split(b"|")
 3     if len(parts) != 3:
 4         return None
 5     signature = _create_signature_v1(secret, name, parts[0], parts[1])
 6     if not _time_independent_equals(parts[2], signature):
 7         gen_log.warning("Invalid cookie signature %r", value)
 8         return None
 9     timestamp = int(parts[1])
10     if timestamp < clock() - max_age_days * 86400:
11         gen_log.warning("Expired cookie %r", value)
12         return None
13     if timestamp > clock() + 31 * 86400:
14         gen_log.warning("Cookie timestamp in future; possible tampering %r",
15                         value)
16         return None
17     if parts[1].startswith(b"0"):
18         gen_log.warning("Tampered cookie %r", value)
19         return None
20     try:
21         return base64.b64decode(parts[0])
22     except Exception:
23         return None

版本2解碼:

 1 def _decode_signed_value_v2(secret, name, value, max_age_days, clock):
 2     try:
 3         key_version, timestamp, name_field, value_field, passed_sig = _decode_fields_v2(value)
 4     except ValueError:
 5         return None
 6     signed_string = value[:-len(passed_sig)]
 7 
 8     if isinstance(secret, dict):
 9         try:
10             secret = secret[key_version]
11         except KeyError:
12             return None
13 
14     expected_sig = _create_signature_v2(secret, signed_string)
15     if not _time_independent_equals(passed_sig, expected_sig):
16         return None
17     if name_field != utf8(name):
18         return None
19     timestamp = int(timestamp)
20     if timestamp < clock() - max_age_days * 86400:
21         # The signature has expired.
22         return None
23     try:
24         return base64.b64decode(value_field)
25     except Exception:
26         return None

注:須要說一下的是因爲版本 1 的設計缺陷,沒有對 timestamp 進行簽名,爲了儘量防止攻擊者篡改時間戳來進行攻擊, _decode_signed_value_v1 函數對 timestamp 執行了額外的檢查(timestamp > clock() + 31 * 86400),但這個檢查並不能徹底杜絕此類攻擊。這應該也是從新設計版本 2 的一個緣由。

 

Tornado中XSRF源碼分析:

  經過上面的Cookie分析後,知道不一樣版本的Cookie含有的相應組成字段,若是咱們想要使用XSRF機制的話,咱們須要在Application的Settings中設置參數:「xsrf_cookie_version」。咱們會在Cookie中,設置一個「_xsrf」字段,而後全部的POST請求中包含一個「_xsrf」字段,若是其與服務器上的「_xsrf」值沒法匹配,那麼服務器會認爲其有一個潛在的跨域僞造風險而拒絕表單的提交。從而防止跨域請求僞造。

  在tornado.web.RequestHandler 中與生成跨站請求僞造 token 直接相關的是 xsrf_token 屬性和 xsrf_form_html 方法

xsrf_token() 模塊:

 1 def xsrf_token(self):
 2     if not hasattr(self, "_xsrf_token"):
 3         version, token, timestamp = self._get_raw_xsrf_token()
 4         output_version = self.settings.get("xsrf_cookie_version", 2)
 5         cookie_kwargs = self.settings.get("xsrf_cookie_kwargs", {})
 6         if output_version == 1:
 7             self._xsrf_token = binascii.b2a_hex(token)
 8         elif output_version == 2:
 9             mask = os.urandom(4)
10             self._xsrf_token = b"|".join([
11                 b"2",
12                 binascii.b2a_hex(mask),
13                 binascii.b2a_hex(_websocket_mask(mask, token)),
14                 utf8(str(int(timestamp)))])
15         else:
16             raise ValueError("unknown xsrf cookie version %d",
17                              output_version)
18         if version is None:
19             expires_days = 30 if self.current_user else None
20             self.set_cookie("_xsrf", self._xsrf_token,
21                             expires_days=expires_days,
22                             **cookie_kwargs)
23     return self._xsrf_token

首先,經過 _get_raw_xsrf_token() 方法,從cookie中解析出相應的字段:

 1 def _get_raw_xsrf_token(self):
 2     if not hasattr(self, '_raw_xsrf_token'):
 3         cookie = self.get_cookie("_xsrf")
 4         if cookie:
 5             version, token, timestamp = self._decode_xsrf_token(cookie)
 6         else:
 7             version, token, timestamp = None, None, None
 8         if token is None:
 9             version = None
10             token = os.urandom(16)
11             timestamp = time.time()
12         self._raw_xsrf_token = (version, token, timestamp)
13     return self._raw_xsrf_token

找到名爲 「_xsrf」 的cookie,而後經過 _decode_xsrf_token() 方法解碼出 (version,token,timestamp)以元祖的形式返回,同時其會對版本1進行兼容(版本1沒有timestamp和version字段),:

 1 def _decode_xsrf_token(self, cookie):
 2     try:
 3         m = _signed_value_version_re.match(utf8(cookie))
 5         if m:
 6             version = int(m.group(1))
 7             if version == 2:
 8                 _, mask, masked_token, timestamp = cookie.split("|")
10                 mask = binascii.a2b_hex(utf8(mask))
11                 token = _websocket_mask(
12                     mask, binascii.a2b_hex(utf8(masked_token)))
13                 timestamp = int(timestamp)
14                 return version, token, timestamp
15             else:
16                 raise Exception("Unknown xsrf cookie version")
17         else:
18             version = 1
19             try:
20                 token = binascii.a2b_hex(utf8(cookie))
21             except (binascii.Error, TypeError):
22                 token = utf8(cookie)
23             timestamp = int(time.time())
24             return (version, token, timestamp)
25     except Exception:
26         gen_log.debug("Uncaught exception in _decode_xsrf_token",
27                       exc_info=True)
28         return None, None, None

xsrf_token檢測check_xsrf_cookie模塊:

對 xsrf_token 的檢查在 _execute 方法(僅僅顯示部分代碼)中委託 check_xsrf_cookie 方法進行,代碼以下所示:

1 def _execute(self, transforms, *args, **kwargs):
2     ......
3     if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
4             self.application.settings.get("xsrf_cookies"):
5         self.check_xsrf_cookie()
6     ......
 1 def check_xsrf_cookie(self):
 2     token = (self.get_argument("_xsrf", None) or
 3              self.request.headers.get("X-Xsrftoken") or
 4              self.request.headers.get("X-Csrftoken"))
 5     if not token:
 6         raise HTTPError(403, "'_xsrf' argument missing from POST")
 7     _, token, _ = self._decode_xsrf_token(token)
 8     _, expected_token, _ = self._get_raw_xsrf_token()
 9     if not token:
10         raise HTTPError(403, "'_xsrf' argument has invalid format")
11     if not _time_independent_equals(utf8(token), utf8(expected_token)):
12         raise HTTPError(403, "XSRF cookie does not match POST argument")

check_xsrf_cookie 方法代碼顯示與 cookie 中的 token 進行比較的 token 來源於請求參數 _xsrf 或者 HTTP 頭域(X-Xsrftoken 或者 X-Csrftoken)。目前僅比較 token 值,對其中的 timestamp 和 version 字段不作比較驗證。

 

最後對xsrf_form_html方法進行介紹:

xsrf_form_html 就是返回一個隱藏的 HTML < input/> 元素,用於包含在頁面的 Form 元素中以便在 POST 請求時將 token 發送給服務端驗證。

它定義了「_xsrf」輸入值,其會檢查全部POST要求防止跨站點請求僞造。若是在Application中的settings中已經設置好了「xsrf_cookies=True」,那麼必須在全部HTML表單中的包含該HTML函數。

在template中,這個方法能夠被調用經過 「{%module xsrf_form_html()%}」

1 def xsrf_form_html(self):
2     return '<input type="hidden" name="_xsrf" value="' + \
3         escape.xhtml_escape(self.xsrf_token) + '"/>'
相關文章
相關標籤/搜索