flask source parsing: session

Keywords: Python Session encoding curl

This is one of the articles in the flask Source Parsing series, a list of all articles in this series:

session Introduction

Before parsing the implementation of session, let's introduce how session is used. Session can be seen as a way to save data between different requests, because HTTP is a stateless protocol, but in business applications we want to know whether different requests are initiated by the same person. For example, when a user clicks into a shopping cart on a shopping website, the server needs to know which user is performing the operation.

It's also easy to use session in flask. As long as you import this variable from flask import session, you can interact with session directly in your code by reading and writing it.

from flask import Flask, session, escape, request

app = Flask(__name__)
app.secret_key = 'please-generate-a-random-secret_key'


@app.route("/")
def index():
    if 'username' in session:
        return 'hello, {}\n'.format(escape(session['username']))
    return 'hello, stranger\n'


@app.route("/login", methods=['POST'])
def login():
    session['username'] = request.form['username']
    return 'login success'


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

The above code simulates a very simple login logic. The user visits POST/login to login, and GET/, when visiting the page later, returns the user's name. Let's take a look at some specific examples of operations (the following are all used) httpie When executed, the same effect can be achieved by using curl commands:

With direct access, we can see the return to hello stranger:

➜  ~ http -v http://127.0.0.1:5000/
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Host: 127.0.0.1:5000
User-Agent: HTTPie/0.8.0


HTTP/1.0 200 OK
Content-Length: 14
Content-Type: text/html; charset=utf-8
Date: Wed, 01 Mar 2017 04:22:18 GMT
Server: Werkzeug/0.11.2 Python/2.7.10

hello stranger

Then we simulate the login request, - v is a print request, - f is to tell the server that this is form data, - session=mysession is to save the requested cookie and other information into this variable, which can be used to specify session:

➜  ~ http -v -f --session=mysession POST http://127.0.0.1:5000/login username=cizixs
POST /login HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Content-Length: 15
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: 127.0.0.1:5000
User-Agent: HTTPie/0.8.0

username=cizixs

HTTP/1.0 200 OK
Content-Length: 13
Content-Type: text/html; charset=utf-8
Date: Wed, 01 Mar 2017 04:20:54 GMT
Server: Werkzeug/0.11.2 Python/2.7.10
Set-Cookie: session=eyJ1c2VybmFtZSI6ImNpeml4cyJ9.C5fdpg.fqm3FTv0kYE2TuOyGF1mx2RuYQ4; HttpOnly; Path=/

login success

Most importantly, we see the head of Set-Cookie in response, the key of cookie is session, and the value is a bunch of seemingly random strings.

Continue. At this point, we use the -- session=mysession parameter to bring the request with the information stored in mysession. After login, we can see the login username.

➜  ~ http -v --session=mysession http://127.0.0.1:5000/
GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Cookie: session=eyJ1c2VybmFtZSI6ImNpeml4cyJ9.C5fevg.LE03yEZDWTUMQW-nNkTr1zBEhKk
Host: 127.0.0.1:5000
User-Agent: HTTPie/0.8.0


HTTP/1.0 200 OK
Content-Length: 11
Content-Type: text/html; charset=utf-8
Date: Wed, 01 Mar 2017 04:25:46 GMT
Server: Werkzeug/0.11.2 Python/2.7.10
Set-Cookie: session=eyJ1c2VybmFtZSI6ImNpeml4cyJ9.C5feyg.sfFCDIqfef4i8cvxUClUUGQNcHA; HttpOnly; Path=/

hellocizixs

This time, notice that in the request sent, the client takes the Cookie header, and the value above saves the value set to us by the response of the previous request.

Summarize: session is implemented by setting cookies on the client side. Every time a client sends a request, all cookies are attached, and some important information (such as user information) is stored in it, so that the server side can know the client's information, and then make corresponding judgments based on these data, as if there is a record between different requests. Recalling.

analysis

We know what session is all about. This section will analyze how flask implements it.

Request process

It is not difficult to imagine that the general parsing process of session is as follows:

  • When a request comes in, flask creates a session variable based on cookie information (if the cookie does not exist, it may be empty) and stores it in the context of the request.

  • View function can get information in session and realize its own logical processing.

  • flask writes the response back to the cookie based on the session value when it sends it

Note: In the process of session and cookie transformation, security should be taken into account, otherwise the direct use of forged cookies will be a great security hazard.

stay The article in flask context In RequestContext, we know that every time a request comes in, the request and session variables we access are variables of the RequestContext instance. At the end of the RequestContext.Push() method, there is a code like this:

self.session = self.app.open_session(self.request)
if self.session is None:
    self.session = self.app.make_null_session()

It initializes the session variable and saves it on RequestContext so that it can be used directly from flask import session. If the secret_key variable is not set, the open_session returns None. At this time, make_null_session is called to generate an empty session. This special session cannot perform any read and write operations, otherwise it will report an exception to the user.

Let's look at the open_session method:

def open_session(self, request):
    return self.session_interface.open_session(self, request)

In Flask, all session-related calls are forwarded to method calls of self.session_interface (so that users can control session usage with custom session_interface). The default session_inerface has a default value:

session_interface = SecureCookieSessionInterface()

Later, when we encounter session-related method explanations, we will directly explain the code implementation of SecureCookieSession Interface and skip the forwarding instructions in the middle.

null_session_class = NullSession

def make_null_session(self, app):
    return self.null_session_class()

def open_session(self, app, request):
    # session Signature Acquisition Algorithms
    s = self.get_signing_serializer(app)
    if s is None:
        return None

    # Get the value of session variable from cookie
    val = request.cookies.get(app.session_cookie_name)
    if not val:
        return self.session_class()

    # Because cookie data needs to be verified for tampering, a signature algorithm is needed to read the values inside.
    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()

open_session retrieves the corresponding session object based on the cookie in the request. The reason for the app parameter is that cookies are validated according to security settings in app, such as signature algorithm and secret_key.

Here are two points that need special explanation: how does the signature algorithm work? How exactly is the session object defined?

session object

The default session object is SecureCookieSession, which is a basic dictionary with some special attributes, such as permanent (flask plug-in will use this variable), modified (indicating whether the instance has been updated, if updated, the cookie will be recalculated and set, because the calculation process is expensive, so if the object has not been modified, it will be directly. Skip over).

class SessionMixin(object):
    def _get_permanent(self):
        return self.get('_permanent', False)

    def _set_permanent(self, value):
        self['_permanent'] = bool(value)

    #: this reflects the ``'_permanent'`` key in the dict.
    permanent = property(_get_permanent, _set_permanent)
    del _get_permanent, _set_permanent

    modified = True

class SecureCookieSession(CallbackDict, SessionMixin):
    """Base class for sessions based on signed cookies."""

    def __init__(self, initial=None):
        def on_update(self):
            self.modified = True
        CallbackDict.__init__(self, initial, on_update)
        self.modified = False

How do you know that instance data has been updated? SecureCookieSession is based on werkzeug/data structures: CallbackDict. This class can specify a function as an on_update parameter, which is called every time a dictionary operation is performed (_setitem_, _delitem_, clear, popitem, update, pop, setdefault).

NOTE: CallbackDict is a clever implementation, but it's not complicated. You can refer to the code for your own interest. The main idea is to overload some of the dictionary update operations, so that they do the original thing at the same time, an additional call to implement a saved function.

For developers, session s can be viewed simply as dictionaries, and all operations are consistent with dictionaries.

signature algorithm

In the process of obtaining cookie data, the core words are:

s = self.get_signing_serializer(app)
val = request.cookies.get(app.session_cookie_name)
data = s.loads(val, max_age=max_age)

return self.session_class(data)

Both sentences are related to s, signing_serializer guarantees security during cookie and session conversion. If flask finds that the requested cookie has been tampered with, it will give up using it directly.

Let's continue with the get_signing_serializer method:

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)

We see that many parameters are needed here:

  • Secret_key: Key. This is a must. If you don't configure secret_key, using session directly will cause an error.

  • Salt: Set up a salt string to enhance security (you can search for "safe salt" by yourself to understand the corresponding principle)

  • serializer: sequential algorithm

  • signer_kwargs: Other parameters, including the digest/hash algorithm (default is sha1) and the signature algorithm (default is hmac)

URLSafeTimedSerializer is itsdangerous Classes of libraries are mainly used for data validation to increase the security of data in the network. itsdangerours provides a variety of serializers that facilitate data serialization and deserialization operations similar to json processing. As for the specific implementation, because of space constraints, it is not explained.

Response process

Flask parses the cookie value automatically when the request comes in and turns it into a session variable. Development can use its values in view functions or update it. Finally, in response, flask automatically writes sessions back to cookies. Let's see how this part works!

Previous articles The process of response is explained, in which the finalize_response method calls the process_response method after generating the response object based on the return of the view function. The process_response method ends with two sentences:

def process_response(self, response):
    ...
    if not self.session_interface.is_null_session(ctx.session):
        self.save_session(ctx.session, response)
    return response

This is where session appears in the response, and the idea is simple. If necessary, call save_sessoin to save the session object in the current context to response.

The code for save_session corresponds to open_session:

def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)
        path = self.get_cookie_path(app)

        # If session becomes an empty dictionary, flask deletes the corresponding cookie s directly
        if not session:
            if session.modified:
                response.delete_cookie(app.session_cookie_name,
                                       domain=domain, path=path)
            return

        # Do you need to set cookies? If the session changes, the cookie must be updated, otherwise the user can `SESSION_REFRESH_EACH_REQUEST'variable to control whether to set the cookie.
        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)

This code is also easy to understand, that is, get all the information you need from the app and session variables, and then call response.set_cookie to set the final cookie. In this way, the client can save session-related information in the cookie and send it to the server again when accessing later, so as to achieve stateful interaction.

Decrypt session

Sometimes in the process of development or debugging, you need to know exactly what value is saved in the cookie, and you can parse its value manually. The session value in the cookie is a string, divided into three parts by a period. The first part is base64 encrypted data, the second part is timestamp, and the third part is verification information.

The contents of the first two parts can be obtained in the following way, and the code can also be intuitive. No explanation is given.

In [1]: from itsdangerous import *

In [2]: s = 'eyJ1c2VybmFtZSI6ImNpeml4cyJ9.C5fdpg.fqm3FTv0kYE2TuOyGF1mx2RuYQ4'

In [3]: data, timstamp, secret = s.split('.')

In [4]: base64_decode(data)
Out[4]: '{"username":"cizixs"}'

In [5]: bytes_to_int(base64_decode(timstamp))
Out[5]: 194502054

In [7]: time.strftime('%Y-%m-%d %H:%I%S', time.localtime(194502054+EPOCH))
Out[7]: '2017-03-01 12:1254'

summary

The session function provided by flask by default is still very simple and meets the basic functions. But we can see that Flask saves session data in the client cookie, where only the user name is OK. If there are some private data (such as password, account balance, etc.), it will cause serious security problems. Consider using flask-session This tripartite library stores data on the server side (local files, redis, memcached), and the client only gets a session ID.

Session is mainly used to save information between different requests, the most common application is the login function. Although the login function can be written out directly through session itself, it can be considered in the actual project. flask-login This tripartite plug-in facilitates our development

Reference material

Posted by doofystyle on Sat, 13 Apr 2019 23:21:32 -0700