【免费下载链接】mailcheap
Newsletters where you pay for the emails you send, not for dormant subscribers, at a fraction of the price of beehiiv and the like. Open source: self-host it, or use mailcheap.ai.
MailCheap 是一个开源的 Newsletter(邮件通讯)系统:你只为实际发出的邮件付费,而不是为沉睡的订阅者付费,价格仅为 beehiiv 等同类服务的一小部分。它支持自托管,也可直接使用 mailcheap.ai。当一个平台承载成千上万个彼此独立的租户(账户)时,"Alice 绝不能碰到 Bob 的订阅者"就成了一条生死线。本文完整拆解 MailCheap 的多租户安全设计:从数据库层的账户隔离守卫、统一权限执行层,到 API Key 的哈希存储、双级别权限与即时撤销,并附带它每次发版都在跑的"双账户渗透测试"。
为什么 Newsletter 系统必须做多租户隔离
每个 Newsletter 作者都是一个租户:他们的订阅者名单、草稿、发送历史、上传的图片、API Key 彼此必须完全隔离。一旦隔离失效,后果是灾难性的——攻击者可以读取甚至删除别人的订阅者名单(那可是受 GDPR 保护的个人信息),或者用别人的 Newsletter 发垃圾邮件毁掉共享的域名声誉。
MailCheap 的做法不是"在每个页面记住过滤一下",而是把隔离下沉到数据库访问层:任何绕过守卫的查询在代码层面根本不可能发出。
账户隔离第一道防线:查询必须携带账户 ID
核心实现只有 54 行,在 lib/account-scope.js 中。它通过一个 Mongoose 插件accountScoped完成三件事:
- 强制字段:所有客户数据模型(Contact、Campaign、Newsletter、ApiKey、Upload…)自动带上必填且不可变的
accountId字段; - 查询守卫:给
find、findOne、updateMany、deleteOne、aggregate、bulkWrite等 12 种数据库操作全部挂上pre钩子,过滤器里没有accountId的查询直接抛异常,而不是返回空结果(见 assertScoped):
function assertScoped(modelName, filter, options) { if (options?.crossAccount || isAccountId(filter?.accountId)) return; throw new Error(`${modelName} query without an accountId`); }- 显式豁免:Webhook、签名链接这类"按唯一键找文档"的场景必须显式声明
{ crossAccount: true },让每一次跨账户访问在代码里都"有名有姓"。
这个设计有个心理学上的妙处:它把"忘了写过滤条件"从一个静默的越权漏洞变成了一个立即崩溃的运行时错误——忘写比写错安全得多。
统一执行层:三条入口,一套检查
MailCheap 的仪表盘、REST API 和 MCP 服务共享同一个执行器 lib/actions/run-action.js。所有功能都先注册成标准 action(用 zod 描述输入输出),再由runAction统一走完同样的检查流水线:
| 检查顺序 | 内容 | 拦截什么 |
|---|---|---|
| 1 | 账户是否处于删除等待期 | 已删除账户的一切调用 |
| 2 | API Key 级别是否够 | signups级 Key 调用full级操作 |
| 3 | 账户是否被封禁 | 垃圾信号触发的封禁 |
| 4 | 订阅计划是否有效 | 无付费计划的写操作 |
| 5 | 限流(按 Key、按账户) | 刷接口、滥用 |
| 6 | 输入校验(zod) | 畸形数据 |
这意味着同一段安全规则不会因为入口不同而有缝隙:网页上能做的事、API 上不能做,或者反过来。项目甚至有一条测试:如果仪表盘的 server action 绕过了注册 action 体系直接操作数据库,测试就失败。
API Key 全生命周期:哈希存储、双级别、即时撤销
这是 MailCheap 安全设计中教科书级的部分,代码见 lib/api-key-tokens.js 与 lib/actions/api-keys.js。
生成与存储。Key 是mc_前缀加 32 字节密码学随机数(generateApiKey),数据库里只存 SHA-256 哈希——即使整库泄露,攻击者拿到的也是一堆不可逆的摘要,无法冒充任何租户。明文 Key 只在创建时返回一次,之后再也无法查看,列表页只显示前缀mc_a1b2c3…供辨认(models/ApiKey.js)。
双级别权限。Key 分full和signups两级:
full:可执行所有操作,但只能存在于服务器端;signups:只能新增订阅者,专门留给嵌入网站代码的 Key。
配合一条精妙的 CORS 规则:所有端点中只有订阅接口对浏览器开放跨域(lib/api/rest.js)。也就是说,就算有人手滑把fullKey 写进了前端页面,浏览器里的恶意脚本也调不动它——CORS 会先拦下来。此外每个账户最多 20 个活跃 Key,每个 Key 限 300 请求/分钟。
撤销即生效。撤销操作(revoke_api_key)只是给记录打上revokedAt时间戳,而鉴权查询(authenticateRequest)永远带revokedAt: null条件——下一个请求到达时该 Key 就自动失效,无需遍历任何缓存或调用任何服务。Key 甚至可以撤销自己,操作幂等,被封禁的账户也允许撤销 Key("止损"永远优先)。
登录与会话:一个验证码不能两用
MailCheap 基于 Better Auth 构建(lib/auth.js),几个值得借鉴的细节:
- 会话存数据库而非纯 JWT:登出或吊销立即生效,不存在"JWT 还没过期"的窗口期;
- 验证码分型:登录码和删除账户专用码是两种不同的 OTP 类型,登录码删不了账户,删除码也登不进系统;验证码 6 位、5 分钟过期、3 次试错作废、存储时哈希;
- 自托管白名单:私有部署只放行
ALLOWED_EMAILS中的邮箱,且在用户创建钩子和会话创建钩子双重拦截(refuseUnlessAllowed)——任何人被移出白名单,下一个请求就会被签出,他的 API Key 同步失效; - 防批量注册:云版限制同一 IP 每天最多 3 个新账户,一次性邮箱域名直接拒绝,且拒绝发生在验证码证明邮箱归属之后,不会向攻击者泄露"这个邮箱已注册"。
删除账户的完整链条:立即冻结、30 天后清除
删除账户(lib/account-deletion.js)是整条链路里体现"纵深防御"最好的环节:
- 只有属主本人能删——它刻意不被注册成 action,所以 API Key 和已授权的第三方应用无法替你删除账户;
- 需要输入专用验证码 + 手打邮箱地址,且 Stripe 仍在扣费、有发送中活动时拒绝删除;
- 请求一确认,立即执行:全部设备会话踢出(signOutEverywhere)、所有 API Key 撤销、已连接应用断开、邮件服务商处的租户挂起、账户在所有入口按"未付费"处理;
- 清除作业以签名的 QStash 任务形式提前发出,30 天后执行;期间重新登录可恢复账户,一旦清除开始则不可逆(restoreAccount);
- 清除作业里的每一条查询都带着
accountId——清除一个租户绝不会误删隔壁的数据。
每次发版都在跑的"渗透测试"
这套设计最有力的证明是 tests/isolation.test.mjs:测试创建 Bob 的完整世界(Newsletter、订阅者、活动、API Key、上传文件),然后让已登录的 Alice 用应用提供的所有通道去碰它:
server action:改 Bob 的 Newsletter → { error: "Newsletter not found" } REST API:用 Alice 的 Key 往 Bob 的列表加订阅者 → 404 守卫直测:Contact.find({ email: "reader1@example.org" }) → 抛异常 结果:Bob 的数据快照前后完全一致,一封测试邮件都没发出去注意两个安全细节:越权尝试统一回答 "not found" 而非 "forbidden",不向攻击者泄露资源的存在性;Alice 在过滤器里点名Bob 的 Newsletter 也查不到任何数据——因为accountId条件是守卫强制注入的,业务代码想"不小心"泄露都难。
上手体验 MailCheap
MailCheap 采用 AGPL 开源协议,git clone https://gitcode.com/gh_mirrors/mai/mailcheap后按仓库内配置文档即可自托管。自托管模式下没有计费墙,但多租户隔离、Key 撤销、封禁与删除链条完整保留——你可以直接跑一遍隔离测试,亲眼看着 Alice 的每一次越权尝试都被拦下。
如果你想给自己的 Newsletter 平台做安全设计,MailCheap 给出的公式可以抄作业:
- 隔离规则写在数据访问层,用"抛异常"而非"返回空";
- 所有入口(Web、API、第三方应用)共享同一个权限执行器;
- 凭证只存哈希、明文只见一次、撤销是无状态立即生效的;
- 用双账户对抗测试把"越权"变成 CI 里的一个断言。
更多细节可参考仓库的安全说明 SECURITY.md 与隔离测试 tests/isolation.test.mjs。
【免费下载链接】mailcheap
Newsletters where you pay for the emails you send, not for dormant subscribers, at a fraction of the price of beehiiv and the like. Open source: self-host it, or use mailcheap.ai.
相关推荐
Chorus安全设计详解:OIDC登录、API Key管理与多租户数据隔离
Chorus安全设计详解:OIDC登录、API Key管理与多租户数据隔离 Chorus 是一款面向 AI 与人类协作的 Agent Harness(智能体协作
抖音TikTok数据采集免费方案:DouK-Downloader从下载到分析的完整上手指南
抖音TikTok数据采集免费方案:DouK Downloader从下载到分析的完整上手指南 你有没有遇到过这种时刻:刷到一个创作者,作品全是你喜欢的风格,想把他
网页爬虫CLI短视频Cortex 读路径 Shuffle Sharding 设计解析:从写路径 subring 到全链路租户隔离
Cortex 读路径 Shuffle Sharding 设计解析:从写路径 subring 到全链路租户隔离 Cortex( README.md https:/
可观测性时序数据库后端指标监控
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考