There are loads of tutorials/examples of how to implement server side storage using flask_session, but I cannot figure out how to prevent client side access to the session. Imagine I want to store data related to a client, but without that client ever having access to the data. I can store that in the server side session to persist across page visits, but I've noticed that I can easily access this from the client side, via jinja: {{session}}
{{session}} prints the entirety of the session to the user - is there a way to prevent this? Or to make certain variables "private?" Perhaps sessions are the wrong way to go about this entirely?
Rather new to this, so any advice would be great.
CodePudding user response:
TL;DR: You're doing everything right and what you describe isn't a vulnerability.
By default with Flask you're using server-side rendering, which means that your program composes the HTML in its entirety before sending it out to the user's web browser. To that end, it uses a templating engine, and in Flask that's most commonly Jinja.
Because pages might want to have user-specific content on them, it makes sense that they should be able to access the session object. But the user does not get to see the template code, or the parameters supplied to the template -- they only see the final, "rendered" HTML, with all the substitutions already made. The fact that you can use the session object in your template is not a security vulnerability -- that is, by design, one of the functions of the session object.
So how do you know if the client can see your session? You test it! Specifically, we will have a simple app that records your name and PIN, and which should echo only the last digit of your PIN back to you, and see what data is available in the client's cookies. (This is only for demo purposes -- for examples of proper secret storage, please look elsewhere.)
templates/index.html
:
<html>
<body>
{% if 'name' in session %}
<p>Your name is: {{session['name']}}</p>
{% else %}
<p>Your name is unset.</p>
{% endif %}
{% if 'pin' in session %}
<p>Your PIN ends in: {{session['pin'][-1]}}</p>
{% else %}
<p>Your PIN is unset.</p>
{% endif %}
</body>
</html>
test1.py
:
from flask import Flask, session, render_template
app = Flask(__name__)
app.config['SECRET_KEY'] = b'notverysecret'
@app.route('/')
def get_name():
return render_template('index.html')
@app.route('/set/<name>/<pin>')
def set_name(name, pin):
session['name'] = name
session['pin'] = pin
return f'Your name has been set to {name} and your PIN now ends with {pin[-1]}.\n'
if __name__ == '__main__':
app.run('127.0.0.1', 5000)
Let's run the server program and use curl
to issue requests to the server. The -b cookies.txt -c cookies.txt
flags mean that we will send the cookies stored in cookies.txt
to the server with our request, and save the cookies we get back in the same file -- just like how the web-browser uses them.
$ curl -b cookies.txt -c cookies.txt http://localhost:5000/set/danya02/1234
Your name has been set to danya02 and your PIN now ends with 4.
$ curl -b cookies.txt -c cookies.txt http://localhost:5000/
<html>
<body>
<p>Your name is: danya02</p>
<p>Your PIN ends in: 4</p>
</body>
</html>
$ cat cookies.txt
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 0 session eyJuYW1lIjoiZGFueWEwMiIsInBpbiI6IjEyMzQifQ.YZiywg.EnQTwefXVM33JguMPHmST-WK1bU
The server has given us the cookie that we can use to later retrieve the name and last digit of the PIN, but what does that cookie contain? It contains the string eyJuYW1lIjoiZGFueWEwMiIsInBpbiI6IjEyMzQifQ.YZiywg.EnQTwefXVM33JguMPHmST-WK1bU
, which is three Base64 strings separated by dots. And if you convert the first one of them to text...
$ base64 -d
eyJuYW1lIjoiZGFueWEwMiIsInBpbiI6IjEyMzQifQ
{"name":"danya02","pin":"1234"}base64: invalid input
...then, despite the complaint from the base64
program, we can recover the data in the cookie. (The other two parts in the cookie are a cryptographic signature that ensures that the client can't tamper with the values stored in it without the server noticing.)
This is exactly what you're trying to avoid -- the client must not be able to see this information. And you're correct in that the flask_session
library is the way to do it. So, let's edit the server program to use it instead of the default session object. Luckily, because of how that library was built, it requires very little change in our code:
test2.py
:
from flask import Flask, session, render_template
from flask_session import Session # <-- added
app = Flask(__name__)
SESSION_TYPE = 'filesystem' # <-- added
app.config.from_object(__name__) # <-- added
Session(app) # <-- added
@app.route('/')
def get_name():
return render_template('index.html')
@app.route('/set/<name>/<pin>')
def set_name(name, pin):
session['name'] = name
session['pin'] = pin
return f'Your name has been set to {name} and your PIN now ends with {pin[-1]}.\n'
if __name__ == '__main__':
app.run('127.0.0.1', 5000)
Let's now do the same test with this new server program:
$ rm cookies.txt
$ curl -b cookies.txt -c cookies.txt http://localhost:5000/set/danya02/1234
Your name has been set to danya02 and your PIN now ends with 4.
$ curl -b cookies.txt -c cookies.txt http://localhost:5000/
<html>
<body>
<p>Your name is: danya02</p>
<p>Your PIN ends in: 4</p>
</body>
</html>
$ cat cookies.txt
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 1640076681 session 0222c14c-b34b-4467-87ec-7618f8110766
This time, we have something different in our cookie -- some kind of UUID. That gives us no information about what we've written into the session, because the mapping between the UUIDs and the actual variables is stored on the server (in this case, in some file in the flask_session
folder, although that can be changed -- see the library's configuration options for more details).
In short, this means that our session is indeed stored on the server, and the client cannot access it. But it is still accessible to the server, and specifically to the template, and that's okay because the template only shows the information that the client is meant to see. So as long as you don't decide to leak the session object -- say, by having a bare {{session}}
in a template -- then your session information is only ever going to be accessible by the server, which is exactly what you want.
But because it is so easy to leak the session object, you should not use it to store truly sensitive information, even though it's only visible to the server. For important information, you want some kind of storage where every action you do -- such as reading, writing, creating or deleting -- is something you have to explicitly opt into. For that, you need a database of some description, and there are many resources on how to do that -- you can start with the Flask tutorial application, which uses it for user-submitted data.