Python安全编码规范 (Python Secure Coding Standards)
分类: SDL规范文档 · 阅读时间: 约11分钟 · 最后更新: 2026-08-14

1. 概述

本规范面向使用 Python 进行 Web 后端、自动化脚本和数据处理的开发团队, 给出可直接落地的安全编码规则和代码示例。内容涵盖注入防护、不安全反序列化、服务端请求伪造 (SSRF)、路径遍历、模板注入、密码学误用以及依赖与密钥管理。所有规则延续安全编码总则中的通用原则, 并结合 Python 语言与主流框架 (Flask、Django、FastAPI) 的特性给出具体指导。

版本说明
本规范基于 Python 3.11 及以上版本。部分标准库行为 (如 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)
注意
如果业务确实需要构造 shell 命令, 对每个用户可控参数使用 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)

口令存储严禁使用 md5sha1 等快速哈希。应使用专为口令设计的算法, 如 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-auditsafety 扫描已知漏洞依赖, 作为流水线的安全门禁。
  • requirements.txtpoetry.lock 中锁定版本与哈希, 保证构建可复现, 防止依赖被替换。
  • 密钥、令牌和数据库口令严禁硬编码进源码或提交到版本库。通过环境变量或密钥管理服务读取。
# 危险: 硬编码密钥
API_KEY = "sk_live_9f3a...c210"

# 安全: 从环境变量读取, 缺失即失败
API_KEY = os.environ["API_KEY"]

10. 小结

Python 安全编码的核心可以概括为: 参数化查询防 SQL 注入, 列表参数防命令注入, 跨信任边界拒绝 pickle, 校验用户提供的 URL 与文件路径, 区分模板变量与模板本身以防 SSTI, 使用 secrets 与口令专用哈希, 以及将依赖扫描和密钥外置纳入工程流程。建议将上述规则固化进代码审计 Checklist, 并在 SAST 工具规则集中启用对应检查项, 实现自动化的安全编码合规。

陈立 (Chen Li)
Python Security Specialist
应用安全工程师, 长期从事 Python Web 后端与自动化系统的安全评审。关注注入类漏洞、供应链安全与 SAST 规则工程, 参与维护开源静态分析规则集, 在 KCon、HITB 等安全会议上分享过 Python 应用安全实践。