1. 概述
本规范面向使用 Python 进行 Web 后端、自动化脚本和数据处理的开发团队, 给出可直接落地的安全编码规则和代码示例。内容涵盖注入防护、不安全反序列化、服务端请求伪造 (SSRF)、路径遍历、模板注入、密码学误用以及依赖与密钥管理。所有规则延续安全编码总则中的通用原则, 并结合 Python 语言与主流框架 (Flask、Django、FastAPI) 的特性给出具体指导。
hashlib 的 usedforsecurity 参数、tomllib 的引入) 在旧版本中不可用。第三方库版本请以各项目的安全公告为准。
2. SQL注入防护
2.1 使用参数化查询
Python 中防御 SQL 注入的首要规则与其他语言一致: 使用数据库驱动提供的参数化查询, 禁止使用 f-string、% 或 + 拼接 SQL 语句。参数占位符由驱动负责转义, 用户输入永远不会被解释为 SQL 语法。
错误示例 (存在SQL注入):
# 危险: f-string 直接拼接用户输入
name = request.args.get("name")
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
正确示例:
# 安全: 参数化查询, 由驱动负责转义
name = request.args.get("name")
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
# SQLite 使用 ? 占位符
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
2.2 ORM的安全使用
使用 SQLAlchemy 或 Django ORM 时, 优先使用其查询构造器和字段过滤方法。如确需编写原生 SQL, 仍必须使用绑定参数, 禁止字符串拼接:
# 危险: 拼接进 text() 语句
db.execute(text("SELECT * FROM users WHERE id = " + user_id))
# 安全: 使用绑定参数
db.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
3. 命令注入防护
调用外部程序时, 最常见的漏洞来自 shell=True 配合字符串拼接。规则是: 使用列表形式传递参数并保持 shell=False (默认值), 让操作系统按参数边界解析, 而不是交给 shell 重新分词。
错误示例 (存在命令注入):
# 危险: 拼接后交给 shell 解析
host = request.args.get("host")
os.system("ping -c 1 " + host)
subprocess.run(f"ping -c 1 {host}", shell=True)
正确示例:
# 安全: 列表参数, 不经过 shell
host = request.args.get("host")
subprocess.run(["ping", "-c", "1", host], shell=False, timeout=5)
shlex.quote() 进行转义, 但这应作为最后手段而非首选方案。
4. 不安全的反序列化
Python 的 pickle 模块可以在反序列化过程中执行任意代码, 因此严禁对不可信数据调用 pickle.loads()。同类风险还包括 yaml.load() 的默认全加载模式和 marshal。
错误示例:
# 危险: 反序列化不可信数据可导致远程代码执行
data = pickle.loads(request.get_data())
config = yaml.load(user_input) # 默认 Loader 可实例化任意对象
正确示例:
# 安全: 跨信任边界的数据使用 JSON
data = json.loads(request.get_data())
# 必须用 YAML 时, 使用 safe_load
config = yaml.safe_load(user_input)
原则: 跨信任边界传输结构化数据时, 使用 JSON 等仅表达数据、不携带代码语义的格式。pickle 只用于完全受信的内部场景。
5. 服务端请求伪造 (SSRF)
当应用根据用户输入发起 HTTP 请求 (如 URL 预览、Webhook、图片抓取) 时, 必须校验目标地址, 否则攻击者可诱导服务访问内网或云平台元数据接口 (如 169.254.169.254)。
错误示例 (存在SSRF):
# 危险: 直接请求用户提供的 URL
url = request.json["callback_url"]
resp = requests.get(url)
正确示例:
# 安全: 解析并校验目标地址
import ipaddress, socket
from urllib.parse import urlparse
def is_safe_url(url):
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
return False
ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
return not (ip.is_private or ip.is_loopback or ip.is_link_local)
if is_safe_url(url):
resp = requests.get(url, timeout=5, allow_redirects=False)
更稳健的做法是使用域名白名单, 并禁用或严格限制重定向, 因为重定向可绕过一次性的地址校验。
6. 路径遍历防护
使用用户输入构造文件路径时, ../ 序列可使访问越出预期目录。规则是: 解析出绝对真实路径后, 校验它仍位于允许的基目录之内。
错误示例 (存在路径遍历):
# 危险: 用户可通过 ../ 读取任意文件
filename = request.args.get("file")
return open(os.path.join(BASE_DIR, filename)).read()
正确示例:
# 安全: 校验解析后的真实路径未逃逸基目录
filename = request.args.get("file")
base = os.path.realpath(BASE_DIR)
target = os.path.realpath(os.path.join(base, filename))
if os.path.commonpath([base, target]) != base:
abort(403)
return open(target).read()
处理上传文件名时, 可使用 werkzeug.utils.secure_filename() 剥离危险字符。
7. 模板注入与XSS
Flask 默认的 Jinja2 与 Django 模板均默认对输出进行 HTML 自动转义, 这是防御 XSS 的基础。真正的高危错误是服务端模板注入 (SSTI): 将用户输入拼接进模板字符串本身, 而非作为变量传入。
错误示例 (存在SSTI):
# 危险: 用户输入成为模板的一部分, 可执行表达式
name = request.args.get("name")
return render_template_string("Hello " + name)
正确示例:
# 安全: 用户输入作为变量传入, 由模板负责转义
name = request.args.get("name")
return render_template("hello.html", name=name)
另外, 避免对用户内容使用 | safe 过滤器或 mark_safe(); 如确需渲染富文本, 应先经过 bleach 等库的白名单清洗。
8. 密码学与随机数
Python 标准库中 random 模块是伪随机, 不可用于安全场景。凡涉及令牌、会话标识、密码重置码等, 必须使用 secrets 模块。
# 危险: random 可预测
token = str(random.random())
# 安全: 使用 secrets 生成密码学随机值
token = secrets.token_urlsafe(32)
口令存储严禁使用 md5 或 sha1 等快速哈希。应使用专为口令设计的算法, 如 argon2 (argon2-cffi) 或 bcrypt, 它们内置加盐和可调计算成本:
# 安全: 使用 argon2 存储口令哈希
from argon2 import PasswordHasher
ph = PasswordHasher()
hashed = ph.hash(password)
ph.verify(hashed, password_attempt)
对称加密请使用经过审计的高层接口, 如 cryptography 库的 Fernet, 避免自行拼装 AES 的模式与填充。不要自造加密算法。
9. 依赖与密钥管理
Python 应用的攻击面很大程度来自第三方依赖。应将依赖安全纳入 CI 流程:
- 使用
pip-audit或safety扫描已知漏洞依赖, 作为流水线的安全门禁。 - 在
requirements.txt或poetry.lock中锁定版本与哈希, 保证构建可复现, 防止依赖被替换。 - 密钥、令牌和数据库口令严禁硬编码进源码或提交到版本库。通过环境变量或密钥管理服务读取。
# 危险: 硬编码密钥
API_KEY = "sk_live_9f3a...c210"
# 安全: 从环境变量读取, 缺失即失败
API_KEY = os.environ["API_KEY"]
10. 小结
Python 安全编码的核心可以概括为: 参数化查询防 SQL 注入, 列表参数防命令注入, 跨信任边界拒绝 pickle, 校验用户提供的 URL 与文件路径, 区分模板变量与模板本身以防 SSTI, 使用 secrets 与口令专用哈希, 以及将依赖扫描和密钥外置纳入工程流程。建议将上述规则固化进代码审计 Checklist, 并在 SAST 工具规则集中启用对应检查项, 实现自动化的安全编码合规。