仅仅是发送一封邮件?暴露安全边界!
2025年6月9日 10:52
🕳️ 背景:从简单的“发封邮件”开始
本来只是个普通的需求:
“用脚本写一个,给用户发封提醒邮件就行,流程简单点。”
我想都没多想,就接入了邮箱的 SMTP 服务,并愉快地获取了邮箱授权码(对,就是那个不是密码,但比密码更危险的东西)。然后写下了这段代码——请注意,这是错误示范:
// frontend/mail.js(错误示范!!!)
const smtpToken = "xqyuafxxxxxxbx"; // 邮箱授权码
fetch("https://smtp.qq.com/send", {
method: "POST",
headers: {
Authorization: `Bearer ${smtpToken}`,
},
body: JSON.stringify({
to: "xxx@qq.com",
subject: "Test",
text: "Hello from frontend",
})
});
在 localhost
上一切正常,邮件发出、无异常。直到我部署到了生产环境,点开浏览器 DevTools 的那一刻,头皮发麻:
- Token 明文出现在请求 header 中
- token 字符串直接写死在 JS 文件里
- Chrome Sources 面板能轻松全文搜索出
smtpToken
这意味着——任何访问我网站的人,都能顺手牵羊进我邮箱。
我试着用 curl 复现了一下,只要有这个 token,连密码都不需要,直接能登录邮箱,读取邮件、伪造发信、批量投毒,真·一秒社会工程。
为什么这是致命的安全漏洞?
邮箱授权码 = “绕过密码的万能钥匙”:
- 对于 QQ、163 等邮箱,一旦授权码泄露,相当于失去了邮箱控制权
- 邮箱常被用于系统找回密码、验证码校验、用户注册绑定,一旦被入侵,牵一发而动全身
- 如果资源被收录(如 GitHub Copilot、Shodan),攻击者可以批量扫描、自动化提取 token,用于攻击
🧠 错误的本质:混淆了“前端能看到”与“后端能做”的边界
我当时想都没想的逻辑是这样的:
“我前端只是在发个请求,token 我加密一下不就行了?”
这是典型的开发者误区: 前端加密 = 把锁和钥匙一起交出去。
前端代码运行在用户设备上,任何 JS、加密逻辑、混淆方法都逃不过 DevTools、F12、抓包器和爬虫——你终究得把 token 还给用户设备,那就是等于公开。
✅ 正确的做法:前端发起“意图”,后端执行“动作”。我们要遵守 “最小信任原则”:
🟢 前端代码(只发送意图)
fetch("/api/send-email", {
method: "POST",
body: JSON.stringify({
to: "user@example.com",
subject: "你好",
body: "邮件内容"
})
});
🟢 后端代码(执行安全操作)
def send_email(to, subject, body):
server = smtplib.SMTP_SSL("smtp.qq.com", 465)
server.login("your@email.com", os.getenv("EMAIL_KEY"))
server.sendmail(...省略...)
关键点:
- 授权码保存在后端
.env
文件,不出现在任何 JS 文件中 - 发送邮件动作由后端执行,可加权限、频控、黑名单
- 前端只负责触发意图,无需关心 SMTP、Token、登录
🧰 安全架构图:前端 <-> 后端 <-> 邮箱服务器
🧪 安全版代码 Demo
🔐 .env 文件(服务器本地)
EMAIL_USER=your@qq.com
EMAIL_PASS=your_authorization_code
SMTP_SERVER=smtp.qq.com
SMTP_PORT=465
⚠️ 一定不要提交
.env
到 Git 仓库,加到.gitignore
!
🖥️ Flask 后端(app.py)
from flask import Flask, request, jsonify
from flask_cors import CORS
from email.mime.text import MIMEText
import smtplib, os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
CORS(app)
EMAIL_USER = os.getenv("EMAIL_USER")
EMAIL_PASS = os.getenv("EMAIL_PASS")
SMTP_SERVER = os.getenv("SMTP_SERVER")
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
@app.route("/api/send-email", methods=["POST"])
def send_email():
data = request.get_json()
to, subject, body = data.get("to"), data.get("subject"), data.get("body")
if not to or not subject or not body:
return jsonify({"success": False, "error": "缺少字段"}), 400
try:
msg = MIMEText(body, "plain", "utf-8")
msg["Subject"], msg["From"], msg["To"] = subject, EMAIL_USER, to
server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT)
server.login(EMAIL_USER, EMAIL_PASS)
server.sendmail(EMAIL_USER, [to], msg.as_string())
server.quit()
return jsonify({"success": True})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
if __name__ == "__main__":
app.run(port=5001)
📋 安全编码 CheckList(务必贴墙上!)
项目 | 能否出现在前端 | 说明 |
---|---|---|
接口地址 | ✅ 可以 | 推荐 proxy 转发 |
公钥 | ✅ 可以 | 非对称加密可用 |
私钥/授权码 | ❌ 禁止 | 一旦泄露等于暴露控制权 |
AccessToken | 🚫 限制使用 | 建议短时 + 刷新 |
OAuth Code | 🚫 禁止 | 不要保存在前端资源中 |
SMTP Token | ❌ 禁止 | 只能后端持有 |
🧩 小结
安全不是“上线之后加一层校验”,而是“写代码那一刻就该想好的边界”。
OK,以上便是本次分享~
欢迎加我:atar24
,交个盆友,我会第一时间通过。