β

tornado 的 cookie 验证机制

nosa.me 17 阅读

作者 @zhouqiang

这篇文章通过阅读 tornado 源码来探索 tornado 是如何处理 cookie 的。

处理过程简单来说就是验证密码之后服务器端(tornado)返回带有 cookie 信息的 Set-Cookie header 给客户端, 之后客户端发起请求时会把此 cookie 放入 Cookie header 中发给服务器端。


tornado 设置 cookie

首先是对 cookie 的变量进行设置, Morsel 是含有几个特殊 key 的类似于 dict 的对象

def set_cookie(self, name, value, domain=None, expires=None, path="/", expires_days=None):        
    if not hasattr(self, "_new_cookie"):
        self._new_cookie = Cookie.SimpleCookie()       
    self._new_cookie[name] = value
    morsel = self._new_cookie[name]
    if domain:
        morsel["domain"] = domain
    if expires_days is not None and not expires:
        expires = datetime.datetime.utcnow() + datetime.timedelta(
            days=expires_days)
    if expires:
        morsel["expires"] = httputil.format_timestamp(expires)
    if path:
        morsel["path"] = path

然后将 cookie 的 header flush 给客户端

def flush(self, include_footers=False, callback=None):
    ...
    if hasattr(self, "_new_cookie"):
        for cookie in self._new_cookie.values():
            self.add_header("Set-Cookie", cookie.OutputString(None))
    ...
    return self.request.connection.write_headers(
           start_line, self._headers, chunk, callback=callback)
    ...

torando 读取 cookie 并验证

tornado 从浏览器那获取 cookie 则特别简单,直接取出 header 中 Cookie 字段的内容, 然后解析一下

def cookies(self):
    if not hasattr(self, "_cookies"):
        self._cookies = Cookie.SimpleCookie()
        if "Cookie" in self.headers:
            try:
                parsed = parse_cookie(self.headers["Cookie"])
            except Exception:
                pass
            else:
                for k, v in parsed.items():
                    try:
                        self._cookies[k] = v
                    except Exception:
                        pass
    return self._cookies

以上代码就是 cookie 在 server 和浏览器中传递的过程.当然这只是简单的传递, 很容易找到规律并进行暴力破解进行提权攻击.

tornado 提供了加密的 cookie, cookie 的传递还是上述代码, 唯一的不同是在服务端对 cookie 的 value 进行了加密, 这样用户即使知道其他用户的名字, 也无法在短时间内构造出一条正确的 cookie

tornado 在服务端需要自己定义一个 secret key. 一个加密的 cookie 的 value 由(value, timestamp, signature) 三元组组成, 一般 value 可以通过加密算法构造, 而 timestamp 则直接可以从现有的 cookie 里面直接取, 所以最重要的是 signature 的构造, 由于用户不知道 secret.所以用户无法通过算法构造出 signature, 只能窃取或者通过暴力破解.而 服务端则能够通过 cookie 确认 value 是正常的 value 且能够取出 value 里包含的信息

加密代码

def create_signed_value(secret, name, value):
    clock = time.time
    timestamp = utf8(str(int(clock())))
    value = base64.b64encode(utf8(value))
    signature = _create_signature_v1(secret, name, value, timestamp)
    value = b"|".join([value, timestamp, signature])
    return value
def _create_signature_v1(secret, *parts):
    hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
    for part in parts:
        hash.update(utf8(part))
    return utf8(hash.hexdigest())

解密代码

def _decode_signed_value_v1(secret, name, value, max_age_days, clock):
    parts = utf8(value).split(b"|")
    signature = _create_signature_v1(secret, name, parts[0], parts[1])
    if not _time_independent_equals(parts[2], signature):
        return None
    clock = time.time
    timestamp = int(parts[1])
    if timestamp < clock() - max_age_days * 86400:
        gen_log.warning("Expired cookie %r", value)
        return None
    return base64.b64decode(parts[0])
def _time_independent_equals(a, b):
    for x, y in zip(a, b):
        result |= ord(x) ^ ord(y)
    return result == 0

总结

函数 _time_independent_equals 是很讲究的。 它总是花费同样的时间去比较用户的输入和你计算的结果。比如用户想要暴力构造一些 session, 如果比较函数花费的时间和 signature 前面 n 字节是否正确正(或者负)相关。那么变更signature, 通过大量查看延时, 理论上是能把 signature 暴力破解出来的, 而这个 _time_independent_equals 可以防止这种攻击。

另外, tornado 这种校验 cookie 的方式能够天然解决 cookie 一致性的问题,可以方面的进行水平扩展。

Related posts:

  1. tornado 实现把 session 存储到 redis
  2. Nginx proxy_redirect 指令
  3. 构建机房运维基础架构(十三): 搭建NTP服务
  4. docker 基础镜像的管理
作者:nosa.me
未来不会有sa
原文地址:tornado 的 cookie 验证机制, 感谢原作者分享。

发表评论