一、生产环境Flask项目安全加固的核心逻辑
很多开发者刚接触Flask时,觉得它轻量好上手,随便写个demo就能跑,直接改改往生产环境一放就完事。但生产环境和本地开发完全不是一回事——本地你自己用,没人攻击;生产环境是面向全网的,只要有漏洞,黑帽能在几分钟内扫出来。Flask本身的安全设计其实很完善,但很多人用的时候会忽略官方的安全规范,导致项目变成“软柿子”。
先给大家说个真实的案例:去年有个做社区团购的小公司,用Flask做了个后台管理系统,直接把开发时的debug模式开着就上线了。结果被人扫到debug端口,拿到了服务器的shell权限,整个数据库被删光,损失了几十万。debug模式看起来是个小功能,但在生产环境就是致命漏洞。
那生产环境Flask项目的安全加固到底要做什么?核心逻辑其实就是三句话:关掉不该开的功能、补全该补的防护、限制不该有的权限。接下来我们就分具体的模块说。
二、基础配置的安全加固(最容易忽略的环节)
很多人觉得基础配置都是“小事”,但80%的生产环境Flask漏洞都出在基础配置上。我们就一个个说。
2.1 绝对关闭debug模式
debug模式是Flask给开发者调试用的,能实时重载代码、显示详细的错误栈、甚至能在页面上执行Python代码。但生产环境绝对不能开。
很多人会说“我配置文件里写了debug=False啊”,但有时候会因为环境变量覆盖、配置文件加载顺序错了,导致debug模式还是开着。怎么确认?给大家一个验证方法:用curl请求一个不存在的路由,比如curl http://你的域名/不存在的路径,如果返回的是带Traceback的详细错误,那debug模式肯定开着;如果只返回简单的404页面,才是正常的。
这里给大家一个正确的Flask配置示例,技术栈是Python+Flask,我们用配置类的方式来写,避免配置混乱:
# 技术栈:Python 3.9 + Flask 2.3.3
from flask import Flask
# 生产环境配置类,所有配置都写在这里,避免全局变量混乱
class ProductionConfig:
# 绝对关闭debug,生产环境必须是False
DEBUG = False
# 必须设置SECRET_KEY,这个是Flask所有加密功能的核心
# 不能用硬编码的字符串,必须用随机生成的、足够长的字符串
# 生成方法:在Python终端执行import secrets; print(secrets.token_hex(16))
SECRET_KEY = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" # 这里是示例,实际要用自己生成的
# 限制允许的主机,避免主机头攻击
ALLOWED_HOSTS = ["你的域名.com", "www.你的域名.com"]
# 关闭Flask的静态文件缓存?不对,生产环境要开缓存,但是要配置正确的缓存头
# 这里先不说,后面讲静态文件的时候再说
app = Flask(__name__)
# 加载生产环境配置
app.config.from_object(ProductionConfig)
# 验证debug模式的代码,生产环境可以去掉,开发时用来检查
@app.route('/check-debug')
def check_debug():
return f"Debug模式状态:{app.debug}"
if __name__ == '__main__':
# 生产环境绝对不能用app.run()!必须用Gunicorn、uWSGI这些WSGI服务器
# app.run(host='0.0.0.0', port=5000)
pass
2.2 安全的SECRET_KEY配置
SECRET_KEY是Flask的核心,所有和加密相关的功能,比如session、CSRF令牌、签名Cookie,都靠这个密钥。如果SECRET_KEY泄露了,黑客就能伪造session、篡改Cookie,甚至拿到服务器权限。
很多人配置SECRET_KEY的时候会犯两个错误:一是用简单的字符串,比如"123456"、"secret";二是把SECRET_KEY硬编码在代码里,提交到Git仓库,所有人都能看到。
正确的做法是:SECRET_KEY必须是随机生成的、长度至少16字节的字符串,而且要存在环境变量里,绝对不能提交到代码仓库。给大家一个用环境变量加载SECRET_KEY的示例:
# 技术栈:Python 3.9 + Flask 2.3.3 + python-dotenv 1.0.0
import os
from dotenv import load_dotenv
from flask import Flask
# 加载.env文件里的环境变量,.env文件绝对不能提交到Git,要加到.gitignore里
load_dotenv()
class ProductionConfig:
DEBUG = False
# 从环境变量加载SECRET_KEY,如果环境变量不存在,就抛出错误,避免用默认值
SECRET_KEY = os.getenv("FLASK_SECRET_KEY")
if not SECRET_KEY:
raise ValueError("生产环境必须设置FLASK_SECRET_KEY环境变量!")
ALLOWED_HOSTS = ["你的域名.com", "www.你的域名.com"]
app = Flask(__name__)
app.config.from_object(ProductionConfig)
然后在项目根目录创建.env文件,内容是:
FLASK_SECRET_KEY=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
最后一定要把.env加到.gitignore里,避免提交到Git:
# .gitignore文件内容
.env
__pycache__/
*.pyc
venv/
2.3 限制允许的主机(ALLOWED_HOSTS)
很多人不知道ALLOWED_HOSTS是干嘛的,其实它是用来防止主机头攻击的。什么是主机头攻击?比如黑客可以伪造请求的Host头,比如把Host改成evil.com,如果你的Flask项目没有限制ALLOWED_HOSTS,就会把这个Host当成合法的,可能会导致一系列问题,比如重定向到错误的域名、泄露敏感信息等。
Flask 2.0以后的版本,ALLOWED_HOSTS是默认关闭的?不对,是如果没有设置ALLOWED_HOSTS,Flask会接受任何Host头。所以生产环境必须设置ALLOWED_HOSTS,只允许自己的域名访问。
三、常见漏洞的防御方案(核心内容)
Flask项目的常见漏洞,其实和其他Web项目的漏洞差不多,但Flask有自己的特性,所以防御方法也有针对性。我们就说几个最常见的。
3.1 CSRF漏洞的防御
CSRF是跨站请求伪造,简单说就是黑客诱导用户点击一个链接,在用户不知情的情况下,用用户的身份发送请求。比如用户登录了你的后台,黑客给用户发个链接,用户点了之后,就会自动删除自己的账号。
Flask本身没有内置CSRF防御,所以我们要用Flask-WTF这个扩展来做。Flask-WTF是Flask的表单扩展,内置了CSRF防御功能。
给大家一个完整的CSRF防御示例,技术栈是Python+Flask+Flask-WTF:
# 技术栈:Python 3.9 + Flask 2.3.3 + Flask-WTF 1.2.1
import os
from dotenv import load_dotenv
from flask import Flask, render_template, request, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
load_dotenv()
class ProductionConfig:
DEBUG = False
SECRET_KEY = os.getenv("FLASK_SECRET_KEY")
if not SECRET_KEY:
raise ValueError("生产环境必须设置FLASK_SECRET_KEY环境变量!")
ALLOWED_HOSTS = ["你的域名.com", "www.你的域名.com"]
# Flask-WTF的配置,所有表单都开启CSRF防御
WTF_CSRF_ENABLED = True
# CSRF令牌的过期时间,默认是3600秒(1小时),可以根据需要调整
WTF_CSRF_TIME_LIMIT = 3600
app = Flask(__name__)
app.config.from_object(ProductionConfig)
# 定义一个表单,用来提交数据
class MyForm(FlaskForm):
name = StringField('姓名', validators=[DataRequired(message='姓名不能为空')])
submit = SubmitField('提交')
# 表单页面
@app.route('/form', methods=['GET', 'POST'])
def form():
form = MyForm()
if form.validate_on_submit():
# 表单验证通过,处理数据
name = form.name.data
return f"你提交的姓名是:{name}"
return render_template('form.html', form=form)
if __name__ == '__main__':
pass
然后创建templates/form.html模板文件,内容是:
<!DOCTYPE html>
<html>
<head>
<title>表单示例</title>
</head>
<body>
<form method="POST">
<!-- 这里必须有CSRF令牌,Flask-WTF会自动生成 -->
{{ form.hidden_tag() }}
<div>
{{ form.name.label }}<br>
{{ form.name() }}<br>
{% if form.name.errors %}
<span style="color: red;">{{ form.name.errors[0] }}</span>
{% endif %}
</div>
<div>
{{ form.submit() }}
</div>
</form>
</body>
</html>
这里要注意几个点:一是所有的POST请求的表单,都必须有{{ form.hidden_tag() }},这个会自动生成CSRF令牌;二是如果是AJAX请求,需要把CSRF令牌放到请求头里,Flask-WTF会自动验证。
3.2 XSS漏洞的防御
XSS是跨站脚本攻击,简单说就是黑客在你的网站上注入恶意的JavaScript代码,当其他用户访问这个页面时,就会执行这个恶意代码,比如窃取用户的Cookie、账号密码等。
Flask的Jinja2模板默认是自动转义的,也就是说,如果你在模板里用{{ 变量 }},Jinja2会自动把变量里的特殊字符(比如<、>、"等)转义成HTML实体,这样就不会执行恶意代码。
但很多人会不小心关掉自动转义,比如用{{ 变量 | safe }},这个过滤器会告诉Jinja2这个变量是安全的,不需要转义,这样就会导致XSS漏洞。
给大家一个XSS防御的示例,技术栈是Python+Flask+Jinja2:
# 技术栈:Python 3.9 + Flask 2.3.3
import os
from dotenv import load_dotenv
from flask import Flask, render_template, request
load_dotenv()
class ProductionConfig:
DEBUG = False
SECRET_KEY = os.getenv("FLASK_SECRET_KEY")
if not SECRET_KEY:
raise ValueError("生产环境必须设置FLASK_SECRET_KEY环境变量!")
ALLOWED_HOSTS = ["你的域名.com", "www.你的域名.com"]
app = Flask(__name__)
app.config.from_object(ProductionConfig)
# 模拟一个用户提交的内容,比如用户在评论区提交的内容
@app.route('/xss-test', methods=['GET', 'POST'])
def xss_test():
if request.method == 'POST':
# 用户提交的内容,比如是一个恶意的脚本
user_input = request.form.get('user_input', '')
# 把用户输入保存到模板里
return render_template('xss_test.html', user_input=user_input)
return render_template('xss_test.html', user_input='')
if __name__ == '__main__':
pass
然后创建templates/xss_test.html模板文件,内容是:
<!DOCTYPE html>
<html>
<head>
<title>XSS测试</title>
</head>
<body>
<form method="POST">
<input type="text" name="user_input" placeholder="输入内容">
<button type="submit">提交</button>
</form>
<div>
<!-- 正确的写法:自动转义,不会执行恶意代码 -->
<h3>自动转义的结果:</h3>
<p>{{ user_input }}</p>
</div>
<div>
<!-- 错误的写法:用了safe过滤器,会执行恶意代码,导致XSS漏洞 -->
<h3>用了safe过滤器的结果(危险!):</h3>
<p>{{ user_input | safe }}</p>
</div>
</body>
</html>
大家可以测试一下,比如在输入框里输入<script>alert('你被攻击了')</script>,提交之后,自动转义的结果会把<script>转义成<script>,不会执行;而用了safe过滤器的结果,会直接弹出警告框,说明存在XSS漏洞。
所以生产环境绝对不能随便用safe过滤器,除非你能100%保证变量里的内容是安全的。
3.3 SQL注入漏洞的防御
SQL注入是最常见的Web漏洞之一,简单说就是黑客通过输入恶意的SQL语句,来获取数据库的敏感信息、修改数据库、甚至删除数据库。
Flask本身不自带数据库操作,所以SQL注入的防御完全靠开发者自己。很多人用原生的SQL语句拼接的方式来操作数据库,比如:
# 错误的写法:直接拼接SQL语句,会导致SQL注入漏洞
username = request.args.get('username')
sql = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(sql)
如果黑客输入的username是' OR 1=1 --,那么拼接后的SQL语句就是SELECT * FROM users WHERE username = '' OR 1=1 --',这个语句会返回所有用户的信息,因为OR 1=1永远为真,--会把后面的内容注释掉。
正确的做法是用参数化查询,也就是把SQL语句和参数分开,数据库会自动处理参数里的特殊字符,避免SQL注入。
给大家一个用SQLAlchemy(Flask最常用的ORM)做参数化查询的示例,技术栈是Python+Flask+SQLAlchemy:
# 技术栈:Python 3.9 + Flask 2.3.3 + SQLAlchemy 2.0.25 + python-dotenv 1.0.0
import os
from dotenv import load_dotenv
from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
load_dotenv()
class ProductionConfig:
DEBUG = False
SECRET_KEY = os.getenv("FLASK_SECRET_KEY")
if not SECRET_KEY:
raise ValueError("生产环境必须设置FLASK_SECRET_KEY环境变量!")
ALLOWED_HOSTS = ["你的域名.com", "www.你的域名.com"]
# 数据库配置,这里用MySQL做示例,SQLAlchemy支持所有主流数据库
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URI")
if not SQLALCHEMY_DATABASE_URI:
raise ValueError("生产环境必须设置DATABASE_URI环境变量!")
# 关闭SQLAlchemy的警告
SQLALCHEMY_TRACK_MODIFICATIONS = False
app = Flask(__name__)
app.config.from_object(ProductionConfig)
# 初始化SQLAlchemy
db = SQLAlchemy(app)
# 定义用户模型
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f'<User {self.username}>'
# 创建数据库表(生产环境要用迁移工具,比如Alembic,这里只是示例)
with app.app_context():
db.create_all()
# 正确的写法:用参数化查询,避免SQL注入
@app.route('/get-user', methods=['GET'])
def get_user():
# 获取请求参数
username = request.args.get('username', '')
# 用SQLAlchemy的参数化查询,?是占位符,参数会自动转义
user = db.session.query(User).filter(User.username == username).first()
if user:
return f"用户信息:{user.username}, {user.email}"
return "用户不存在"
if __name__ == '__main__':
pass
这里要注意几个点:一是绝对不能直接拼接SQL语句;二是如果用原生的SQL语句,必须用参数化查询,比如cursor.execute("SELECT * FROM users WHERE username = ?", (username,))(不同数据库的占位符可能不一样,比如MySQL用%s,SQLAlchemy会自动处理);三是生产环境要用ORM,比如SQLAlchemy,ORM会自动帮你做参数化查询,避免SQL注入。
3.4 路径遍历漏洞的防御
路径遍历漏洞,简单说就是黑客通过构造特殊的路径,来访问服务器上的敏感文件,比如/etc/passwd、数据库配置文件等。
比如很多人会写这样的代码,用来读取用户上传的文件:
# 错误的写法:没有限制路径,会导致路径遍历漏洞
@app.route('/read-file', methods=['GET'])
def read_file():
filename = request.args.get('filename', '')
# 直接打开文件,没有限制路径
with open(filename, 'r') as f:
content = f.read()
return content
如果黑客输入的filename是../../etc/passwd,那么这个代码就会读取服务器上的/etc/passwd文件,泄露敏感信息。
正确的做法是限制文件的路径,把所有允许访问的文件都放在一个指定的目录里,然后把用户输入的文件名和这个目录拼接起来,同时要检查拼接后的路径是不是在这个指定的目录里。
给大家一个路径遍历防御的示例,技术栈是Python+Flask:
# 技术栈:Python 3.9 + Flask 2.3.3 + os模块(Python标准库)
import os
from dotenv import load_dotenv
from flask import Flask, request, send_file
load_dotenv()
class ProductionConfig:
DEBUG = False
SECRET_KEY = os.getenv("FLASK_SECRET_KEY")
if not SECRET_KEY:
raise ValueError("生产环境必须设置FLASK_SECRET_KEY环境变量!")
ALLOWED_HOSTS = ["你的域名.com", "www.你的域名.com"]
# 允许访问的文件的根目录,所有文件都必须在这个目录里
# 这个目录要放在项目根目录下,比如./allowed_files,要确保这个目录的权限是正确的
ALLOWED_FILES_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), 'allowed_files'))
app = Flask(__name__)
app.config.from_object(ProductionConfig)
# 正确的写法:限制路径,避免路径遍历漏洞
@app.route('/read-file', methods=['GET'])
def read_file():
filename = request.args.get('filename', '')
# 拼接允许访问的路径
allowed_path = os.path.join(app.config['ALLOWED_FILES_ROOT'], filename)
# 把路径转换成绝对路径,方便检查
absolute_allowed_path = os.path.abspath(allowed_path)
# 检查拼接后的路径是不是在允许的根目录里,如果不是,就返回403
if not absolute_allowed_path.startswith(app.config['ALLOWED_FILES_ROOT']):
return "禁止访问", 403
# 检查文件是否存在
if not os.path.isfile(absolute_allowed_path):
return "文件不存在", 404
# 返回文件
return send_file(absolute_allowed_path)
if __name__ == '__main__':
pass
这里要注意几个点:一是必须把所有允许访问的文件都放在一个指定的根目录里;二是必须检查拼接后的路径是不是在这个根目录里;三是不能把根目录放在服务器的敏感目录里,比如/etc、/root等。
四、生产环境部署的安全加固(最后一道防线)
很多人觉得代码没问题就安全了,其实部署环境的安全也很重要。比如你代码写得再好,服务器的密码是123456,黑客还是能拿到服务器权限。
4.1 用非root用户运行项目
Linux系统的root用户权限很大,一旦被黑客拿到,整个服务器就完了。所以生产环境的Flask项目必须用非root用户运行。
给大家一个部署的示例,技术栈是Python+Flask+Gunicorn+Nginx:
# 技术栈:CentOS 7 + Python 3.9 + Flask 2.3.3 + Gunicorn 21.2.0 + Nginx 1.20.1
# 1. 创建一个非root用户,比如叫flask_user
useradd -m -s /bin/bash flask_user
# 2. 给这个用户设置密码
passwd flask_user
# 3. 把项目目录的权限给flask_user
chown -R flask_user:flask_user /path/to/your/project
# 4. 切换到flask_user,安装依赖
su - flask_user
cd /path/to/your/project
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# 5. 用Gunicorn启动项目,不能用root用户
gunicorn -w 4 -b 127.0.0.1:5000 app:app
# 6. 配置Nginx反向代理,Nginx可以用root用户运行,但Flask项目必须用非root用户
# Nginx的配置文件(/etc/nginx/conf.d/your_domain.conf):
# server {
# listen 80;
# server_name your_domain.com www.your_domain.com;
# location / {
# proxy_pass http://127.0.0.1:5000;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
# }
4.2 配置防火墙和SSL证书
生产环境必须配置防火墙,只开放必要的端口,比如80、443、22(SSH)等,其他端口都关闭。同时必须配置SSL证书,用HTTPS访问,避免数据在传输过程中被窃取。
给大家一个用firewalld配置防火墙的示例:
# 技术栈:CentOS 7 + firewalld
# 1. 启动firewalld
systemctl start firewalld
systemctl enable firewalld
# 2. 开放80、443、22端口
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=ssh
# 3. 重新加载防火墙配置
firewall-cmd --reload
# 4. 查看开放的端口
firewall-cmd --list-all
SSL证书可以用Let's Encrypt免费申请,用certbot工具配置:
# 技术栈:CentOS 7 + certbot 1.22.0 + Nginx 1.20.1
# 1. 安装certbot
yum install certbot python3-certbot-nginx
# 2. 申请SSL证书
certbot --nginx -d your_domain.com -d www.your_domain.com
# 3. 配置自动更新证书
crontab -e
# 每天凌晨2点更新证书
0 2 * * * /usr/bin/certbot renew --quiet
五、应用场景、技术优缺点及注意事项
5.1 应用场景
本文的安全加固方案适用于所有生产环境的Flask项目,不管是小型的个人博客、中型的电商网站,还是大型的企业后台管理系统。尤其是对于涉及用户敏感信息(比如手机号、身份证号、银行卡号)、支付功能的项目,必须严格按照本文的方案进行加固。
5.2 技术优缺点
优点
- 所有方案都是基于Flask官方的安全规范,兼容性好,不会影响项目的正常运行。
- 示例都是用的主流的技术栈,比如SQLAlchemy、Flask-WTF、Gunicorn、Nginx等,学习成本低,容易上手。
- 方案覆盖了从代码到部署的全流程,安全防护比较全面。
缺点
- 部分方案会增加开发的复杂度,比如用Flask-WTF做CSRF防御,需要额外学习Flask-WTF的用法。
- 部署环境的加固需要一定的Linux基础,对于初学者来说可能有一定的难度。
- 安全是一个动态的过程,本文的方案只能防御常见的漏洞,不能保证100%的安全,还需要定期更新依赖、扫描漏洞。
5.3 注意事项
- 所有的配置文件、环境变量都不能提交到Git仓库,要加到
.gitignore里,避免泄露敏感信息。 - 定期更新项目的依赖,比如Flask、SQLAlchemy、Flask-WTF等,及时修复已知的漏洞。
- 定期扫描项目的漏洞,比如用Snyk、OWASP ZAP等工具,及时发现并修复漏洞。
- 生产环境的日志要妥善保存,定期备份,同时要限制日志的访问权限,避免泄露敏感信息。
六、文章总结
生产环境Flask项目的安全加固,不是一蹴而就的事情,而是一个持续的过程。核心是要从代码、配置、部署三个层面入手,全面覆盖常见的漏洞。
代码层面要注意防御XSS、SQL注入、路径遍历等漏洞;配置层面要注意关闭debug模式、安全配置SECRET_KEY、限制ALLOWED_HOSTS;部署层面要注意用非root用户运行项目、配置防火墙、配置SSL证书。
很多开发者觉得安全加固是麻烦事,但和项目被攻击带来的损失相比,这点麻烦根本不算什么。希望大家能重视Flask项目的安全加固,避免因为小漏洞导致大损失。
评论
围绕“生产环境中Flask项目的安全加固及常见漏洞的防御方案”参与讨论