一次安全审计之后:TeleDeck 0.7.15 把哪些东西变成了工程约束

把一批靠'应该没问题'维持的判断,写成代码里可检查的约束:出站 HTTPS-only、字段级隐私边界、有界队列与心跳、对账游标不跳过失败项、并发写入收窄。

工程笔记 · 2026-10-14 · 约 7 分钟阅读 · 标签:Telegram、MTProto、工程实践、TeleDeck、安全审计、隐私边界

一次安全审计之后:TeleDeck 0.7.15 把哪些东西变成了工程约束

之前写过一篇 TeleDeck 的架构记录,讲的是怎么把十个 Telegram 账号工具收进一个容器、共用一份 Client 和 Session。那篇解决的是"怎么放"的问题。

这次不一样。0.7.15 这一轮做的是审计加固——把一些原本靠"应该没问题"维持的判断,改成代码里明确可检查的约束。

审计发现的往往不是 bug,是边界没写下来

代码能跑、功能正确,不代表边界是清晰的。这一轮真正被翻出来的,大多是这类问题:

  • 一个"从 URL 下载媒体"的通用能力,实际能访问的范围比设计意图大;
  • 某段内容被当作证据送进模型时,没有明确说清哪些部分可以进、哪些必须留在本地;
  • 事件队列在正常情况下有界,但在异常路径上没有上限;
  • 一个人为操作失败后,后台的同步游标却跳过了它;
  • 两个并发写入各自持有"旧设置",最后写入的那个把对方的改动覆盖掉了。

这些都是典型的"平时看不出来,出事时很难查"的问题。它们的共同点是:没有一个地方写着这条边界是什么,所以也没法判断有没有被越过。

出站请求必须能说清目的地

最直接的一类是出站请求的收敛。

媒体下载和图片获取都是"给定一个地址就去取"的能力。设计意图是取用户指定的内容,但代码层面如果只做格式校验,实际能取的范围会宽很多——包括内网地址、非预期协议、带凭据的 URL。

这一轮的约束是:只允许公开的 HTTPS 目标。非公网地址、非 HTTPS、URL 里带用户名或密码的情况一律拒绝。Provider 返回的图片下载走同一套限制——模型服务返回的链接同样不可信,不能因为它来自一个可信的 Provider 就放行。

这类约束的价值不在于挡住攻击者,而在于让"这个请求会发到哪里"成为一个可回答的问题。边界写下来之后,判断对错就变成了一件机械的事。

隐私边界要落在具体的字段上

SumPlus 的摘要逻辑需要把聊天内容送给模型,AI 问答需要结合回复上下文。这两件事本身是功能的一部分,问题在于"送什么"如果没有明确规则,就会在迭代中慢慢变宽。

这一轮把边界收在具体的位置上:

  • 进入摘要的证据有明确范围,超出范围的内容不作为依据;
  • AI 引用相关的内容,在进入上下文之前经过过滤;
  • 需要留在本地的部分不进入模型请求,也不落进日志。

"不落进日志"这条容易被忽略。很多隐私问题的实际来源不是接口调用,而是一行用于排障的 debug 输出把所有东西都写进了日志文件。

这里没有引入新的加密或者权限模型,只是把"哪些字段可以离开这台机器"这件事说清楚,然后在代码里守住。

队列要有上限,而且上限要有意义

长期运行的进程里,队列是最容易失控的地方。

正常路径下,事件来得慢、处理得快,队列长度几乎总是 1 或者 2。但在阻塞、限流或者网络抖动的时候,如果入队没有上限,内存就会一路涨上去,直到进程被系统杀掉。而进程被杀之后,最难查的恰恰是"为什么它突然没了"。

这一轮给观察队列和命令队列都加上了明确的边界,并且补上了运行心跳的验证——不只是"进程还活着",而是"它还在正常处理事件"。

有界的代价是需要定义超限时的行为:拒绝新事件、丢弃最旧的、还是阻塞生产者。三种选择各有各自的失败模式,没有免费选项。这里选的是不让内存无界增长的方案,具体行为在运行时文档里有说明。

对账游标不能跳过失败项

Shift 模块负责按规则转发消息,历史消息的对账靠一个有序游标推进。

原实现在遇到处理失败时继续往前推进游标。看起来只是丢了一条消息,实际后果更麻烦:游标一旦越过那条的 ID,后面所有的对账都不会再回头处理它,这条消息就永久性地漏掉了。

改成在第一个失败项处停止这一轮扫描。游标停在原地,之后的扫描会重试这一条。

代价是明确的:一个持续失败的消息会阻塞该来源后续的对账推进,直到某次扫描成功为止。实时转发不受影响,仍然照常工作。

这是一个刻意的取舍。对账这类任务的语义是"最终把所有消息处理完",允许跳过的设计在出问题时不会有任何提示,而阻塞的设计至少会留下一个可观察的现象。

顺带修的另一处是持久化失败的处理:写入失败会返回给调用方,写队列会被恢复以便后续重试,没能成功持久化的游标会被回滚——而不是让内存中的进度和磁盘上的进度对不上。

并发写入不能互相覆盖

AutoName 会按分钟更新昵称,同时又要响应用户的配置变更。这两条路径都会写入设置。

问题出在时序上:一条路径读到了当前设置,然后去执行一次 Telegram I/O;在这段时间里,另一条路径改了别的字段;第一条路径 I/O 回来后把整份设置写回去,第二次的改动就没了。

修法是收窄写入范围——恢复配置完成时只把轮换和时间戳相关的字段应用到当前设置上,而不是替换整个在 I/O 之前捕获的对象。触发限流而临时禁用时同样保留无关的当前设置。

同样的思路也用在停止和清理路径上:停止/清理会等待当前已经调度的任务结束;重置原始配置之前先把它排空,之后再恢复其他启用用户的更新。

这不是加了锁,而是让每次写入只负责自己那部分字段。粒度更小的写入天然减少了互相覆盖的空间。

关机和恢复路径最容易被忽略

长时间运行的容器里,真正难处理的是停止过程。

这一轮覆盖了几个具体的场景:同步的 teardown 流程要正确处理;配置恢复失败时要把状态持久化下来,而不是留在内存里等下次重启时消失;关闭时要把排队中的 live album 冲出去,并在 Client 还可用的窗口内把 album worker 排空。

这些改动没有新功能,但它们的价值在重启之后才会体现——出问题的时候,能对上账比跑得快更重要。

这些修复的代价

审计加固不是只加东西。每一处约束都换掉了一部分灵活性:

  • 出站请求收敛之后,某些原本"能跑"的地址会被拒绝;
  • 队列有界之后,极端情况下会主动拒绝事件;
  • 对账不跳过失败项之后,一个坏消息会拖慢整个来源的推进;
  • 优雅关闭等待 Telegram I/O 完成,停止过程可能变慢。

还有一条必须说清楚的限制:Telegram 投递和本地持久化不是同一个事务。 如果在投递成功之后、本地记录写入之前进程崩溃,重启后这条消息仍然可能被重放一次。这是分布式系统的固有问题,不是这一轮能解决的,所以它被写进了文档,而不是假装不存在。

一些收尾

0.7.15 之后还跟了几个小改动:新闻图片在开关和状态控制下的持久化、DeepSeek 无限制输出配置的处理、以及摘要列表标记的规范化——最后一个特意保持了模板校验的强度,只调整标记格式。

版本记录、部署文档和安全边界说明都跟着更新了。这不是流程上的形式主义:如果约束只存在于代码里,下一个人在改动时就没有依据判断自己有没有破坏它。


回头看,这一轮最有意义的产出不是修了多少个问题,而是把一批模糊的判断变成了写下来的规则:出站请求能去哪里、哪些字段可以离开本机、队列的上限是多少、失败时游标停在哪里。

规则写下来之后,它们才可能被测试覆盖,也才可能在下次改动中被检查。在此之前,它们只是某个人的记忆。

全部文章 · 返回 OneMJJ 首页

OneMJJ · VPS、自托管与 AI 工具与历史维护笔记。少踩坑,多留传家宝。

部分工具由第三方运营;本站命令仅供复制,不会自动执行。运行前请核对环境、阅读源码并备份重要数据。本站可能展示 Google AdSense 广告,详见隐私政策。