最新消息:图 床

客戶端 session 導致的安全問題

COOL IAM 196浏览 0评论

作者:Phith0n
作者博客:https://www.leavesongs.com/PENETRATION/client-session-security.html

在Web中,session是認證用戶身份的憑證,它具備如下幾個特點:

  1. 用戶不可以任意篡改
  2. A用戶的session無法被B用戶獲取

也就是說,session的設計目的是為了做用戶身份認證。但是,很多情況下,session被用作了別的用途,將產生一些安全問題,我們今天就來談談“客戶端session”(client session)導致的安全問題。

0x01 什麼是客戶端session

在傳統PHP開發中,$_SESSION變量的內容默認會被保存在服務端的一個文件中,通過一個叫“PHPSESSID”的Cookie來區分用戶。這類session是“服務端session”,用戶看到的只是session的名稱(一個隨機字符串),其內容保存在服務端。

然而,並不是所有語言都有默認的session存儲機制,也不是任何情況下我們都可以向服務器寫入文件。所以,很多Web框架都會另闢蹊徑,比如Django默認將session存儲在數據庫中,而對於flask這裡並不包含數據庫操作的框架,就只能將session存儲在cookie中。

因為cookie實際上是存儲在客戶端(瀏覽器)中的,所以稱之為“客戶端session”。

0x02 保護客戶端session

將session存儲在客戶端cookie中,最重要的就是解決session不能被篡改的問題。

我們看看flask是如何處理的:

class SecureCookieSessionInterface(SessionInterface):
    """The default session interface that stores sessions in signed cookies
    through the :mod:`itsdangerous` module.
    """
    #: the salt that should be applied on top of the secret key for the
    #: signing of cookie based sessions.
    salt = 'cookie-session'
    #: the hash function to use for the signature. The default is sha1
    digest_method = staticmethod(hashlib.sha1)
    #: the name of the itsdangerous supported key derivation. The default
    #: is hmac.
    key_derivation = 'hmac'
    #: A python serializer for the payload. The default is a compact
    #: JSON derived serializer with support for some extra Python types
    #: such as datetime objects or tuples.
    serializer = session_json_serializer
    session_class = SecureCookieSession

    def get_signing_serializer(self, app):
        if not app.secret_key:
            return None
        signer_kwargs = dict(
            key_derivation=self.key_derivation,
            digest_method=self.digest_method
        )
        return URLSafeTimedSerializer(app.secret_key, salt=self.salt,
                                      serializer=self.serializer,
                                      signer_kwargs=signer_kwargs)

    def open_session(self, app, request):
        s = self.get_signing_serializer(app)
        if s is None:
            return None
        val = request.cookies.get(app.session_cookie_name)
        if not val:
            return self.session_class()
        max_age = total_seconds(app.permanent_session_lifetime)
        try:
            data = s.loads(val, max_age=max_age)
            return self.session_class(data)
        except BadSignature:
            return self.session_class()

    def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)
        path = self.get_cookie_path(app)
        # Delete case. If there is no session we bail early.
        # If the session was modified to be empty we remove the
        # whole cookie.
        if not session:
            if session.modified:
                response.delete_cookie(app.session_cookie_name,
                                       domain=domain, path=path)
            return
        # Modification case. There are upsides and downsides to
        # emitting a set-cookie header each request. The behavior
        # is controlled by the :meth:`should_set_cookie` method
        # which performs a quick check to figure out if the cookie
        # should be set or not. This is controlled by the
        # SESSION_REFRESH_EACH_REQUEST config flag as well as
        # the permanent flag on the session itself.
        if not self.should_set_cookie(app, session):
            return
        httponly = self.get_cookie_httponly(app)
        secure = self.get_cookie_secure(app)
        expires = self.get_expiration_time(app, session)
        val = self.get_signing_serializer(app).dumps(dict(session))
        response.set_cookie(app.session_cookie_name, val,
                            expires=expires, httponly=httponly,
                            domain=domain, path=path, secure=secure)

主要看最後兩行代碼,新建了URLSafeTimedSerializer類 ,用它的dumps方法將類型為字典的session對象序列化成字符串,然後用response.set_cookie將最後的內容保存在cookie中。

那麼我們可以看一下URLSafeTimedSerializer是做什麼的:

class Signer(object):
    # ...
    def sign(self, value):
        """Signs the given string."""
        return value + want_bytes(self.sep) + self.get_signature(value)

    def get_signature(self, value):
        """Returns the signature for the given value"""
        value = want_bytes(value)
        key = self.derive_key()
        sig = self.algorithm.get_signature(key, value)
        return base64_encode(sig)


class Serializer(object):
    default_serializer = json
    default_signer = Signer
    # ....
    def dumps(self, obj, salt=None):
        """Returns a signed string serialized with the internal serializer.
        The return value can be either a byte or unicode string depending
        on the format of the internal serializer.
        """
        payload = want_bytes(self.dump_payload(obj))
        rv = self.make_signer(salt).sign(payload)
        if self.is_text_serializer:
            rv = rv.decode('utf-8')
        return rv

    def dump_payload(self, obj):
        """Dumps the encoded object. The return value is always a
        bytestring. If the internal serializer is text based the value
        will automatically be encoded to utf-8.
        """
        return want_bytes(self.serializer.dumps(obj))


class URLSafeSerializerMixin(object):
    """Mixed in with a regular serializer it will attempt to zlib compress
    the string to make it shorter if necessary. It will also base64 encode
    the string so that it can safely be placed in a URL.
    """
    def load_payload(self, payload):
        decompress = False
        if payload.startswith(b'.'):
            payload = payload[1:]
            decompress = True
        try:
            json = base64_decode(payload)
        except Exception as e:
            raise BadPayload('Could not base64 decode the payload because of '
                'an exception', original_error=e)
        if decompress:
            try:
                json = zlib.decompress(json)
            except Exception as e:
                raise BadPayload('Could not zlib decompress the payload before '
                    'decoding the payload', original_error=e)
        return super(URLSafeSerializerMixin, self).load_payload(json)

    def dump_payload(self, obj):
        json = super(URLSafeSerializerMixin, self).dump_payload(obj)
        is_compressed = False
        compressed = zlib.compress(json)
        if len(compressed) < (len(json) - 1):
            json = compressed
            is_compressed = True
        base64d = base64_encode(json)
        if is_compressed:
            base64d = b'.' + base64d
        return base64d


class URLSafeTimedSerializer(URLSafeSerializerMixin, TimedSerializer):
    """Works like :class:`TimedSerializer` but dumps and loads into a URL
    safe string consisting of the upper and lowercase character of the
    alphabet as well as ``'_'``, ``'-'`` and ``'.'``.
    """
    default_serializer = compact_json

主要關注dump_payloaddumps,這是序列化session的主要過程。

可見,序列化的操作分如下幾步:

  1. json.dumps 將對象轉換成json字符串,作為數據
  2. 如果數據壓縮后長度更短,則用zlib庫進行壓縮
  3. 將數據用base64編碼
  4. 通過hmac算法計算數據的簽名,將簽名附在數據后,用“.”分割

第4步就解決了用戶篡改session的問題,因為在不知道secret_key的情況下,是無法偽造簽名的。

最後,我們在cookie中就能看到設置好的session了:

转载请注明:IAMCOOL » 客戶端 session 導致的安全問題

0 0 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x