阅读视图

发现新文章,点击刷新页面。

仅仅是发送一封邮件?暴露安全边界!

🕳️ 背景:从简单的“发封邮件”开始

本来只是个普通的需求:

“用脚本写一个,给用户发封提醒邮件就行,流程简单点。”

我想都没多想,就接入了邮箱的 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、登录

🧰 安全架构图:前端 <-> 后端 <-> 邮箱服务器

image.png


🧪 安全版代码 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,交个盆友,我会第一时间通过

❌