在现代 Web 开发中,ORM(对象关系映射)框架的普及极大地降低了 SQL 注入的风险。很多开发者(包括以前的我)因此产生了一种错觉:“只要我用了 SQLAlchemy 或 Django ORM,我的后端就是安全的。”

然而,现实往往会给你一记响亮的耳光。最近我在做安全排查时被告知存在“文件注入”和“文件名注入”风险。这让我意识到,Web 安全远不止 SQL 注入那么简单。

今天我们就来聊聊,除了 SQL 注入,开发过程中还有哪些容易被忽视的“注入”陷阱,以及我们该如何防御。


一、 文件名注入与路径遍历 (Path Traversal)

这是很多初级甚至中级开发者最容易踩的坑。

1. 场景还原

假设你有一个上传头像的功能,后端代码可能是这样写的:

1
2
3
4
5
6
7
import os

def upload_avatar(file):
# 直接使用用户上传的文件名
filename = file.filename
save_path = os.path.join('/var/www/uploads', filename)
file.save(save_path)

看起来很正常,对吧?

2. 攻击方式

如果攻击者构造了一个恶意的请求,把文件名改成了这样:../../../../etc/passwd(Linux)或者 ../../windows/system32/cmd.exe(Windows)。

os.path.join 拼接路径时,它会解析 ..,导致文件最终被保存到了 /var/www/uploads/../../../../etc/passwd,也就是 /etc/passwd

后果

  • 覆盖系统关键文件:攻击者可以覆盖你的配置文件、系统脚本,甚至 SSH authorized_keys。
  • 任意文件读取:如果是下载功能,攻击者可以通过 ../../ 读取服务器上的任意文件(源代码、数据库配置等)。

3. 防御方案

核心原则:永远不要信任用户的输入,包括文件名。

在 Python 中,werkzeug 库提供了一个非常好的工具函数 secure_filename

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from werkzeug.utils import secure_filename
import os

def upload_avatar(file):
# 过滤文件名,只保留 ASCII 字符,去除路径分隔符
filename = secure_filename(file.filename)

# 双重保险:生成随机文件名(推荐)
# import uuid
# ext = filename.rsplit('.', 1)[1] if '.' in filename else 'png'
# filename = f"{uuid.uuid4()}.{ext}"

save_path = os.path.join('/var/www/uploads', filename)
file.save(save_path)

如果不使用库,你也必须手动剥离路径分隔符,并限制文件名的字符集(例如只允许字母、数字、下划线)。


二、 命令注入 (Command Injection)

当你的应用需要调用系统命令时,如果参数处理不当,就会引发命令注入。

1. 场景还原

假设你需要做一个简单的网络诊断工具,允许用户输入 IP 进行 Ping 测试。

1
2
3
4
5
6
import os

def ping_host(ip):
# 危险!直接拼接命令
command = f"ping -c 1 {ip}"
os.system(command)

2. 攻击方式

用户输入正常的 IP 127.0.0.1 没问题。
但如果用户输入:127.0.0.1; rm -rf / (Linux) 或 127.0.0.1 & del C:\* (Windows)。

最终执行的命令变成了:ping -c 1 127.0.0.1; rm -rf /
系统会先执行 ping,紧接着执行删除命令。

3. 防御方案

方法一:使用 subprocess 模块并避免 shell=True

这是最推荐的做法。将命令和参数作为一个列表传递,Python 会负责处理参数转义,系统会将参数视为字符串而非可执行命令。

1
2
3
4
5
import subprocess

def ping_host(ip):
# 安全:参数被隔离
subprocess.run(["ping", "-c", "1", ip], shell=False)

方法二:使用专用库代替系统命令

能用 Python 库解决的,绝不调用系统命令。

  • 想 Ping?用 pythonping 库。
  • 想处理视频?用 ffmpeg-python 绑定库,而不是 os.system('ffmpeg ...')

三、 Python 特有的坑:Pickle 反序列化漏洞

这就不是“注入”了,但属于极其严重且容易被忽视的安全问题。

1. 场景还原

为了方便,很多开发者会用 pickle 模块把 Python 对象序列化后存入 Redis 或文件中,读取时再反序列化。

1
2
3
4
5
import pickle

# 读取用户上传的数据(假设)
data = get_user_input()
obj = pickle.loads(data)

2. 攻击方式

pickle 是一个允许执行任意代码的协议。攻击者可以构造一段恶意的字节流,当你的代码执行 pickle.loads() 时,这段字节流里的恶意代码(比如反弹 Shell)就会被立刻执行。

3. 防御方案

核心原则:绝对不要对不可信的数据源使用 pickle.load()pickle.loads()

  • 使用 JSON:如果只是存储数据(字典、列表、字符串、数字),永远优先使用 json。它不仅跨语言通用,而且安全。
  • 签名验证:如果你必须用 pickle(比如在内部可信节点间传输),必须对数据进行加密签名(HMAC),确保数据在传输过程中没有被篡改。

四、 总结

安全开发不仅仅是“防住 SQL 注入”那么简单。作为一个后端开发,我们需要建立起一种“零信任”的思维模式:

  1. 输入即恶意:前端传来的文件名、IP 地址、ID、JSON 数据,统统当作潜在的攻击载体。
  2. 最小权限原则:运行 Web 服务的用户不应该有 root 权限,不应该能访问项目目录以外的文件。
  3. 使用成熟的轮子:不要自己写正则去过滤路径,用 secure_filename;不要自己拼 SQL,用 ORM;不要自己拼 Shell 命令,用 subprocess

希望这篇总结能帮你避开开发路上的那些安全暗礁。