flask:如何防止客户端访问服务器端 flask_session 变量

flask: how to prevent client access to server side flask_session variables

有很多 tutorials/examples 如何使用 flask_session 实现服务器端存储,但我不知道如何防止客户端访问会话。想象一下,我想存储与客户端相关的数据,但客户端永远无法访问这些数据。我可以将其存储在服务器端会话中以在页面访问中保持不变,但我注意到我可以通过 jinja 从客户端轻松访问它:{{session}}

{{session}} 向用户打印整个会话 - 有没有办法防止这种情况发生?或者使某些变量“私有”?也许会话是完全解决这个问题的错误方法?

对此比较陌生,所以任何建议都很好。

TL;DR:您所做的一切都是正确的,您所描述的不是漏洞。

默认情况下,Flask 使用的是 服务器端呈现,这意味着您的程序在将 HTML 发送给用户之前完整地组成了 HTML网页浏览器。为此,它使用了一个模板引擎,在 Flask 中最常见的是 Jinja。

因为页面可能希望在其上包含用户特定的内容,所以它们应该能够访问会话对象是有道理的。但是用户看不到模板代码,也看不到提供给模板的参数——他们只能看到最终的“渲染”HTML,所有替换都已经完成。您可以在模板中使用会话对象这一事实并不是安全漏洞——也就是说,根据设计,会话对象的功能之一。

那么你怎么知道客户端是否可以看到你的会话?你测试一下!具体来说,我们将有一个简单的应用程序来记录您的姓名和 PIN,它应该只将 PIN 的最后一位回显给您,并查看客户端 cookie 中可用的数据。 (这仅用于演示目的——有关正确秘密存储的示例,请查看其他地方。)

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)

让我们运行服务器程序并使用curl向服务器发出请求。 -b cookies.txt -c cookies.txt 标志意味着我们会将存储在 cookies.txt 中的 cookie 与我们的请求一起发送到服务器,并将我们返回的 cookie 保存在同一个文件中——就像网络浏览器如何使用它们一样.

$ 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

服务器已为我们提供了 cookie,我们稍后可以使用该 cookie 检索 PIN 的名称和最后一位数字,但该 cookie 包含什么?它包含字符串 eyJuYW1lIjoiZGFueWEwMiIsInBpbiI6IjEyMzQifQ.YZiywg.EnQTwefXVM33JguMPHmST-WK1bU,它是由点分隔的三个 Base64 字符串。如果您将其中的第一个转换为文本...

$ base64 -d
eyJuYW1lIjoiZGFueWEwMiIsInBpbiI6IjEyMzQifQ
{"name":"danya02","pin":"1234"}base64: invalid input

...然后,尽管 base64 程序抱怨,我们可以恢复 cookie 中的数据。 (cookie 中的其他两部分是加密签名,确保客户端无法在服务器不注意的情况下篡改存储在其中的值。)

这正是您要避免的 -- 客户端不能看到此信息。你是对的,flask_session 库是实现它的方法。因此,让我们编辑服务器程序以使用它而不是默认会话对象。幸运的是,由于该库的构建方式,我们的代码几乎不需要更改:

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)

现在让我们用这个新的服务器程序做同样的测试:

$ 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

这一次,我们的 cookie 中有一些不同的东西 -- 某种 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 以获取更多详细信息)。

总之,这说明我们的session确实是保存在服务器上的,客户端是访问不到的。但服务器仍然可以访问它,特别是模板,这没关系,因为模板只显示客户端应该看到的信息。因此,只要您不决定泄露会话对象——例如,通过在模板中使用裸 {{session}}——那么您的会话信息将只能由服务器访问,这正是你想要什么。

但是因为会话对象很容易泄露,所以你不应该用它来存储真正敏感的信息,即使它只对服务器可见。对于重要信息,您需要某种存储方式,您必须明确选择在其中执行的每项操作(例如读取、写入、创建或删除)。为此,您需要一个具有某种描述的数据库,并且有很多关于如何做到这一点的资源——您可以从 the Flask tutorial application 开始,它将它用于用户提交的数据。