本文件记录 LR-Agent 桌面端(Electron + Node 版 Agent 运行时)的安全加固措施、 残余风险以及尚未闭环的运维事项。改动代码时请同步维护本文件。
- 所有
dangerouslySetInnerHTML注入点统一走src/renderer/utils/sanitizeHtml.ts(基于 DOMPurify)消毒:FileViewer.tsx(mammoth 转换的 docx HTML)markdownCodeComponents.tsx/HighlightedCodeBlock.tsx/AgentFileDiffView.tsx(highlight.js 输出)FileTypeIcon.tsx为内置可信 SVG,加注释锁定来源。
- docx / markdown 内的链接一律只放行
https:,其余协议(javascript:、data:、file:等)在点击时preventDefault;外链统一经主进程isSafeExternalUrl校验。 - CSP:
- 渲染层
index.ejs不再内联宽泛策略,由 webpack 通过.erb/configs/rendererCsp.ts注入;生产环境内联主题脚本使用sha256-哈希, 不使用script-src 'unsafe-inline';开发环境单独放宽以兼容 HMR/React Refresh。 - 主进程
src/main/security/csp.ts通过onHeadersReceived按运行时配置动态拼装connect-src(后端地址 + 本地 agent/MCP origin),并附加X-Content-Type-Options/Referrer-Policy。 - 生产 CSP 含
object-src 'none'、base-uri 'none'、form-action 'none'。
- 渲染层
src/main/preload.ts移除通用invoke(channel, ...)/sendMessage,改为逐方法窄接口, 并对事件 channel 做运行时白名单校验。- 主进程敏感 handler 增加参数校验:
- 文件读取拒绝应用自身凭据文件(
lr-agent.db、mcp.json、session_cache.dat、refresh_token.dat等)。 - 文件/目录操作限定在「授权根」集合内(
src/main/security/authorizedRoots.ts)。 shell.openPath/window.openExternal拒绝可执行/脚本扩展名 (src/shared/dangerousExtensions.ts),协议白名单只放行https:(及配置的后端http:)。setWindowOpenHandler+will-navigate通过web-contents-created统一挂载, 覆盖未来新增的 webContents。
- 文件读取拒绝应用自身凭据文件(
src/main/security/secretStore.ts基于 ElectronsafeStorage加解密,格式v1:<base64>。- LLM Provider API Key(
llm_providers.api_key_encrypted,encryption_key_id = v1)与 MCP 远程服务敏感 header(mcp.json)落盘前加密。 tokenStore/sessionCacheStore去掉了明文回退:safeStorage不可用时失败关闭。src/main/security/migrateSecrets.ts启动时幂等迁移历史明文(v0)数据,失败不阻断启动。
- 本地 MCP Server(
src/main/mcp/server.ts):每次启动生成随机 token,/mcp校验Authorization: Bearer,并校验Host仅为本机、拒绝带Origin的跨源请求;/health保持开放。 - 本地 Agent 运行时(
src/main/agent/):Electron 主进程以utilityProcess.fork启动agentRuntime.js(host.ts),生成随机 token 并经环境变量LR_AGENT_LOCAL_TOKEN注入,同时绑定随机端口。服务侧server.ts对除/health外的全部路由做常量时间 Bearer 校验;未配置 token 时拒绝启动,不对外提供服务(runtime.ts,与迁移前的 Python 版保持同一失败关闭语义)。 渲染层所有发往本地 Agent 的请求统一经src/renderer/config.ts#localAgentFetch携带 Bearer token。 - CORS 启动校验:
allow_credentials=True时禁止cors_origins含*。 - 错误详情不外泄:路由 handler 抛出未预期异常时,响应体只回
{ detail: 'internal_error' },不回传err.message(异常消息常夹带绝对路径与 依赖库内部片段)。完整消息只进服务端日志(onLog→ 主进程),需要细节时看日志。
- 写盘/编辑改为「文本扩展名白名单」(唯一真源:
src/shared/workspaceTextExtensions.ts): 白名单之外的扩展名一律禁止写入,脚本/可执行类型(.bat、.cmd、.ps1、.vbs、.hta、.scr、.jar、.reg、.lnk等)被明确拒绝。 src/main/workspace/workspaceWrite.ts在path.resolve之外补充fs.realpath校验, 防止工作区内符号链接把读写重定向到授权根之外。
src/main/auth/resetDeepLink.ts校验 scheme/路径/ token 字符集与长度(16–2048), 非法输入不入暂存;token 取用即清(takePendingResetToken)。
- 全量单测:
npm test(含 secretStore 加解密与失败关闭、MCP/mcp401/403/放行、写盘白名单与 扩展名拦截、符号链接逃逸、深度链接校验与用后即清、本地 Agent 运行时的 401/放行与常量时间比较)。 - 静态检查:
npm run lint、npm exec tsc。 - Agent 协议契约:
npm run baseline:selftest(起 mock LLM server,校验 SSE chunk 序列与 tool_call 分片重组)。 - 推理侧(
vendor/inference):无单测覆盖,CI 以python -m compileall做语法检查, 并以node scripts/ciCheckInferenceManifests.mjs校验依赖清单与envInstaller.ts常量同步。 - 手工验证建议:dev 下确认新 CSP 不破坏 HMR/React Refresh;构造含
javascript:链接的.docx确认点击无脚本执行;用错误 token 调/mcp与本地 Agent 接口确认 401。
- 自定义协议劫持:
lr-agent://在本机是全局注册的,恶意程序可抢占注册或在应用未运行时 抢先处理链接。客户端无法彻底解决,需后端缩短 reset token 有效期并做一次性绑定。 - safeStorage 不可用:部分 Linux 环境(无 keyring)下
safeStorage.isEncryptionAvailable()为假,此时凭据写入/读取会失败并需要用户重新输入;这是刻意的失败关闭行为。 - 开发环境 CSP 放宽:dev 策略包含
unsafe-inline/unsafe-eval与本地ws:,仅存在于 dev 构建,不会进入发布包。
- Windows:使用 electron-builder 的证书签名,需要在 CI 注入以下 secret(或等价配置):
WIN_CSC_LINK:.pfx证书的路径或 base64 内容WIN_CSC_KEY_PASSWORD:证书密码
- macOS:
afterSign钩子(.erb/scripts/notarize.js)已按 CI 环境变量开启公证,缺失时自动跳过。 需要的 secret:APPLE_ID、APPLE_ID_PASS、APPLE_TEAM_ID。 - 状态:BLOCKED — 证书/Apple 账号尚未提供,未配置时构建产物为未签名(本地开发可正常运行)。 配置上述 secret 后无需修改代码即可启用签名与公证。
- 审计结论(2026-09):当前
vendor/inference中未发现torch.load、pickle.load、np.load(allow_pickle=True)、yaml.load(、eval(、exec(、os.system等高风险调用。(编排侧vendor/local-agent已随迁移删除,其替代实现是 TypeScript,由 CodeQL 的javascript矩阵覆盖。) - 后续政策:新增模型加载必须使用
torch.load(..., weights_only=True)或safetensors, 禁止加载不可信 checkpoint;引入前需补 CodeQL 扫描(python语言矩阵当前即用于覆盖vendor/inference)。
.github/codeql/codeql-config.yml里按 query id 排除了两条已确认的误报:js/cors-misconfiguration-for-credentials(本仓库的 CORS 实现正是该查询文档给出的 「GOOD」写法:显式白名单 + 启动期拒绝*,CodeQL 无法看穿resolveCorsOrigin这层 helper)与js/clear-text-logging(唯一命中点是本地冒烟脚本scripts/agent-baseline/electronForkSmoke.cjs,被判定"敏感"的只有 HTTP 状态码)。- 该文件只做排除、不改查询集;每条排除都附理由与依据链接。若代码结构调整后不再误报, 应删除对应条目。