本地 Node AI 代理安全复盘:回环访问、日志边界与真正的 SSE
本文目录
本篇将一次个人本机 Node 服务的静态代码复核抽象成通用工程经验,不记录项目名称、内部服务名、地址、账号、目录、模型配置、请求内容或任何密钥。复核范围只包括可读的服务源码,不读取环境文件、浏览器配置、运行日志或凭据;也没有连接上游服务做运行时测试。它是一次有边界的代码审阅,不是安全认证。
复核日期:2026-10-18。观察到的默认配置将服务绑定到本机回环地址,并设置了本机请求来源校验;因此不能沿用旧笔记中“服务监听所有网卡”的结论。仍值得改善的点集中在健康信息最小化、日志字段边界,以及请求响应被完整读入后再解析、并非真正增量传输。以下将这些观察转换成可以复用的设计和测试清单。
2026-10-21 当前源码状态
本次只读复核 server.js、security.js、browser-launch.js、private-log.js 与项目自动化测试;没有读取环境文件、README、本机浏览器资料或日志,没有启动浏览器、登录或连接上游。当前 npm test 为 17/17,通过的范围是单元/中间件测试,不代表真实浏览器或线上验收。
当前服务默认绑定 127.0.0.1,并拒绝非 loopback 监听地址;请求中间件检查 socket 对端、Host 与本机 Origin,CORS 也只允许 loopback 来源。浏览器启动现在把可执行文件和参数分开交给 spawn,不再经过 shell;日志文件权限与敏感载荷不入日志有回归测试,但仍缺集中式日志字段白名单。它们是有价值的纵深防护,但本机来源校验不是调用者身份认证,同机其他进程仍可访问;连接已有 CDP 端点时也没有验证端点所属进程和用户目录。
剩余边界也要按当前实现描述:健康接口只返回存活状态和浏览器是否就绪,已经比暴露登录态或会话标识收敛,但仍是未认证的本机状态位;二维码图片响应没有显式 Cache-Control: no-store。JSON 有 10 MB 总体限制,却没有完整的消息/模型/工具参数 schema。页面请求把上游 body 完整读完后才返回,因此兼容 SSE 仍不是增量流;单个浏览器页的互斥队列没有长度上限,也没有多调用方会话隔离。进程处理了 SIGINT,但未处理 SIGTERM;uncaughtException 处理器只记录错误后继续运行。
这些结论来自静态源码和当前 17 项测试;本次没有测量真实网络首事件时间、背压、浏览器身份隔离、二维码缓存行为或 supervisor 关停。
2026-10-19 源码复核:浏览器会话、输入边界与流式行为
本次又对照了一份本地 Node/Puppeteer 代理的可读源码,并运行项目自带测试与 JavaScript 语法检查。审阅遵守最小暴露范围:不读取环境变量文件、本地私有配置、浏览器用户目录或运行日志;不启动浏览器、不登录、不调用上游。npm test 为 14/14 通过,node --check 对服务与安全中间件均通过。这只能说明现有自动化断言和语法检查通过,不是实际浏览器、账号或线上服务验收。
已有的本机网络防护,不等于用户认证
安全中间件要求监听地址解析到 loopback,并检查 socket 对端地址、Host 和精确的本机 Origin;CORS 允许列表也限制在 loopback 来源。这比绑定所有网卡并开放任意来源稳健。不过它保护的是网络来源边界,不识别同一台电脑上的具体调用进程:本机其他进程仍可能直接调用兼容 API。CORS/Origin 也不是身份认证。若要供多个本机用户、浏览器扩展或远程客户端使用,应再加入明确的调用者凭证、权限范围、速率上限和撤销策略;密钥放在服务端配置,不放 URL、前端包或日志。
Chrome 调试协议端口是高权限入口
浏览器初始化会先尝试连接配置的本机 DevTools/CDP 端点,之后才考虑启动独立浏览器。仅凭端点可响应,不能证明它属于本服务创建的进程、预期用户目录或专用登录态。CDP 通常能读写页面、Cookie 和会话,因此共享调试端口可能把另一份浏览器身份交给代理控制。应使用独占端口和独立用户目录,校验目标进程及启动参数,只绑定 loopback,并在连接失败时避免静默接管其他已运行浏览器。不要把调试端口映射到局域网或公网。
较早检查的源码快照曾通过 shell: true 拼接 Chrome 启动命令。当前复核的启动器已改用 spawn(executable, args),不经过 shell,消除了 shell 字符解析这一类风险;但仍应限制可执行文件来源、验证启动失败处理,并确认不会误连到另一份浏览器的调试端口。
兼容 SSE 的响应目前不是端到端真流式
页面请求在浏览器上下文里先完整读取 JSON 或文本,再把完整结果交回 Node;聊天路由随后才生成客户端的 SSE 响应。因此 stream: true 改变了外部返回格式,却没有让上游 token 边生成边到达客户端。首事件要等待完整上游结果,内存随响应体增长,也没有真正的慢客户端背压或浏览器断连到上游的取消传播。若要实现真流式,应从上游 ReadableStream 逐块读取,使用流式 TextDecoder 和可跨 chunk 的 SSE 解析器,边解析边写出;同时把客户端取消、超时、错误和慢消费者一路传到上游,并限制事件大小和总时长。应以真实 HTTP 客户端确认首事件先于完整响应结束到达,不能只检查响应头或 mock 返回值。
API 输入和单页并发假设
JSON body 有整体字节上限,但聊天处理主要检查最终提问文本是否为空;这不等同于对 messages 结构、角色、模型、工具参数、字段长度和数组数量做完整 schema 校验。入口应以运行时 schema 拒绝未知字段和非法消息,并分别设置请求体、对话长度、工具参数与生成时长限制。
对话状态按模型缓存一段时间,浏览器页和互斥队列则是进程级共享资源。串行队列有助于避免同时操作一个页面,却也意味着请求彼此排队;共享会话在多调用方场景下可能串话。当前结构更适合单机、单可信使用者。若扩展为多人服务,需要按经过认证的会话隔离页面/对话,设并发与队列上限、过期清理及取消策略,避免无限累积状态。
二维码、日志与进程退出
承载登录二维码的截图响应应显式设置 Cache-Control: no-store,避免浏览器、代理或共享缓存留存短期凭证;还应限制访问频率、二维码有效期,并在认证完成后立即失效。二维码不是普通图片,而是足以授权登录的短期秘密。
日志实现采用同步写入并按日期分文件。现有测试覆盖请求/响应正文和敏感 URL 不进入日志的预期,但不能据此证明运行目录权限、所有异常路径或部署日志都安全;还需设置文件权限、大小与保留期,并在高频请求下评估同步 I/O 对事件循环的影响。进程目前主要处理交互式中断;生产托管还应处理 SIGTERM 并优雅关闭 HTTP、浏览器和计时器。uncaughtException 后仅记录日志继续运行不是可靠恢复策略,进程状态不确定时应退出并由 supervisor 重启。仓库同时存在多种包管理器锁文件,也应明确唯一安装入口,避免依赖树漂移。
优先级建议:先确认 CDP 端点确实专属且不对外暴露,并为同机不可信调用方定义明确边界;随后补全请求 schema、队列上限与会话隔离;再实现真流式、取消与背压;同时为 QR 响应加禁缓存策略,补齐日志轮转、SIGTERM 优雅关停和单一锁文件策略。无 shell 启动已在当前源码中实现。以上仍是待验证/待补齐的建议,不代表其余改进已经完成。
先画清信任边界
本机服务常见的第一道边界是只监听回环网卡,而不是 0.0.0.0 或公网地址。它能减少局域网和公网直接连接的机会,但不等于身份认证:同一台机器上的其他进程仍可能访问端口;浏览器页面也可能尝试向本机地址发请求;反向代理、端口转发、容器网络或 SSH 隧道会改变原先假设的可达范围。部署说明应该写清楚谁可以访问、经过哪些代理、端口如何暴露,而不是只写“仅本机”。
Host 与 Origin 校验适合作为浏览器请求的一层防护,但不能代替调用者认证。Origin 可能不存在,非浏览器客户端可以自行构造请求头;跨域策略也主要约束浏览器读取响应,不是网络访问控制。若服务被网页、局域网其他设备或反向代理访问,应增加明确的调用者身份验证、权限范围、速率限制与审计策略,并让代理逐跳保留可信的来源边界。只在可信环境中使用的本机服务,也要避免被误配置成公网接口。
推荐在部署清单中逐项确认:监听地址、主机防火墙、容器端口映射、反向代理路由、TLS 终止位置、认证由哪一层负责、服务账号权限,以及从外部网络验证端口不可达的方式。每次把服务从个人电脑迁到 NAS、云主机、容器或开发机时,都要重新做这张边界图。配置变化、隧道和代理拓扑会让“本地”一词失去原有含义。
健康端点只回答必要问题
健康端点通常只需要回答进程是否存活,以及依赖是否可用。若读者并不需要知道浏览器是否已启动、当前登录状态、用户名称、配置路径、版本明细或上游错误,就不要把这些数据放进公开响应。内部运维可以通过受保护的管理通道读取更详细的 readiness 信息;面向探活器的 liveness 响应则保持稳定、简短、无个人状态。不要为了方便前端判断而顺手暴露浏览器会话和依赖拓扑。
进一步检查缓存与响应头:健康结果是否可能被 CDN 或共享缓存保存,错误是否带出堆栈和上游正文,匿名探测是否会触发昂贵的依赖请求。readiness 可以区分进程运行与核心依赖可用,但应对访问者只返回必要状态;敏感细节写入权限受控的监控系统,并设置保留时长。对外的状态码和字段应稳定,不要把健康接口做成未授权的诊断面板。
日志可观测,但不要复刻对话
AI 代理日志很容易在调试时记录整段用户提示、模型回复、浏览器页面、Cookie、Authorization、上游错误正文或长期可关联的会话 ID。这些内容可能含个人信息、业务数据、凭据和提示注入载荷。默认只记录请求阶段、耗时、状态码、字节数、模型类别等经过评估的元数据;对请求 ID 使用随机且短生命周期的关联值,不要直接使用用户 ID、会话 ID 或上游原始标识。
结构化日志要有集中式字段白名单和脱敏规则,而不是在每个日志点临时删几个字段。错误对象可能包含请求配置、响应头和响应正文,直接序列化整个对象并不安全。对输入长度、字段类型、换行符和控制字符做约束,防止日志注入;生产与开发采用不同详细度。访问日志、应用日志和追踪数据要分别评估访问权限、保留期限、删除流程与备份覆盖范围。
可用以下检查问题做一次日志走查:是否能从日志恢复出原始对话?是否出现 Authorization、Cookie、邮箱、完整 URL query、系统提示或上游响应片段?关联 ID 是否跨天稳定?异常路径是否会绕过统一脱敏?测试时用唯一的虚构标记发送请求,再搜索测试日志确认标记没有落盘;不要用真实用户或生产会话做这类验证。
SSE 必须从上游流到客户端
接口名称里有 stream,或响应头写了 text/event-stream,都不能证明它是实时流。若上游响应先通过 response.text() 读成完整字符串,之后才按换行解析,首字节延迟会接近整段响应下载完成时间,内存占用也随响应大小增长;这属于事后解析,不是端到端流式传输。真正的流式路径需要从上游响应体逐块读取、边解析边向客户端发送,并在客户端断开时取消上游读取。
SSE 是基于 UTF-8 文本的事件格式,不是“每个网络块恰好一条消息”。TCP、HTTP 分块和 JavaScript ReadableStream 的 chunk 边界都可能切在任意字节,包括多字节汉字、data: 前缀或换行符中间。健壮解析器需要使用流式 TextDecoder,缓存未完成片段,正确处理 LF/CRLF、注释行、重复 data: 字段、空行分隔、结束时残留片段和 [DONE] 等应用层约定。不要用 chunk.toString() 或按单个换行符分隔每个 chunk。
端到端还要处理背压与生命周期:客户端变慢时不要无限堆积队列;达到最大事件大小或持续时间时应可控地终止;客户端取消、超时或上游失败时要释放 reader、定时器与连接;代理要谨慎设置缓冲和压缩策略,避免中间层重新合并事件。上游错误若发生在已经发送响应头之后,不能再伪装成普通 JSON 错误,应按既定事件协议发送安全的错误事件,然后结束连接。
建议把流式实现拆成可单测的字节解码、事件解析、转发和取消四层。测试中把同一事件随机切成不同 chunk,覆盖中文 UTF-8 边界、CRLF、多个 data 行、注释、连续事件、末尾没有换行、空响应、超大事件、上游中断、客户端主动取消、慢消费者、超时和错误响应。再用真实 HTTP 客户端测量首个事件时间和内存增长;只测解析函数或 mock 的完整字符串不足以证明在线路上真的逐步输出。
代理上游时再检查 SSRF 与资源上限
如果调用者能提供目标 URL,代理就可能变成 SSRF 跳板。优先使用服务端维护的固定主机 allowlist,而不是接受任意 scheme、主机和端口;解析 DNS 后检查 IPv4/IPv6 地址范围,阻止 loopback、私网、链路本地和云元数据地址;连接时必须固定到已验证的解析结果或使用等效的连接层校验,避免检查与连接之间的 DNS 重绑定竞态;遇到重定向时重新验证每一跳,防止合法域名跳向内部地址。拒绝非预期协议、用户信息字段和混淆主机名,并统一设置连接、首字节、总时长、响应体字节数及并发上限。
即使目标地址固定,也要限制提示和响应的最大大小、请求频率、同时进行的流数量和上游重试次数。无限重试、无上限缓冲和客户端断开后继续生成,都会被放大为资源耗尽。重试只应用于明确可重试且幂等的阶段,并设总预算、抖动退避和熔断;流已经向客户端输出部分结果后,不应悄悄重试并拼接第二次响应。
一份可以落地的验收表
- 网络面:服务默认只绑定回环;公网、局域网、容器宿主机和代理拓扑分别验证;需要远程调用时显式加认证与 TLS,不依赖 CORS 当作访问控制。
- 鉴权面:未认证、过期身份、错误权限和高频请求都有可预期响应;密钥不写入代码、URL、前端包和日志,轮换流程可以实际演练。
- 信息面:外部健康端点只有最小状态;异常响应、访问日志、trace 和监控标签均经过字段白名单审查。
- 流式面:从上游响应体开始逐块转发;首事件能在总响应结束前到达;慢客户端、主动取消、上游失败和超时都会释放资源。
- 输入面:目标 URL、重定向、DNS、文件/响应大小、并发、超时和重试都有明确限制,测试包含 IPv4 与 IPv6 变体。
- 运维面:部署文档记录监听地址、端口、代理与防火墙责任人、健康探测、日志保留、恢复方式及最后验证时间。
本次匿名化复核确认了当前代码已有回环监听、socket/Host/Origin 校验、无 shell 的浏览器启动和日志文件权限测试;仍需关注本机调用者认证边界、CDP 端点归属、二维码禁缓存、消息 schema、队列/会话隔离、真正增量 SSE 与 SIGTERM 生命周期。静态代码和 17 项自动化测试不证明防火墙、代理、浏览器运行态或生产配置符合预期;没有读取环境配置或运行日志,也没有对外部服务做渗透或可达性测试。
参考资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。