阅读视图

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

Memo Code 安全设计:子进程、命令防护与权限审批的统一方案

同步至个人站点:Memo Code 安全设计:子进程、命令防护与权限审批的统一方案

202622

Memo Code 是我最近两个多月投入较多精力的 Agent 项目。类似于Claude Code 和 Codex 的 轻量级本地编程 Agent,目前已具备 Coding Agent 完备技能。

如果你感兴趣的话,欢迎参与:Memo Code - Github,或者给个 Star 鼓励一下哈哈~

做 Agent 这类能「替用户干活」的工具,安全性是躲不掉的坎。

我一开始做 memo(github.com/minorcell/m…)的时候,安全问题还没想那么多——能跑起来就行。后来工具越加越多,shell 命令也越跑越复杂,就开始踩坑了:

  • 子进程忘了关,内存慢慢涨
  • rm -rf / 差点真被我跑出来
  • 每次执行都要点批准,用户体验稀碎

这些问题逼着我认真设计了整套安全方案。今天把思路和实现细节都分享出来,希望对你有帮助。

先想清楚:安全设计要解决什么问题?

我把它拆成三件事:

  1. 资源可控:子进程不能无限开,不能忘了关
  2. 操作安全:危险命令要拦截,误操作要有缓冲
  3. 权限平衡:该拦的拦住,该放的放行,还要给用户留个「后门」

下面逐一展开。

第一道防线:子进程管理——防止内存泄漏与资源耗尽

memo 的 shell 执行用的是 Node.js 的 child_process.spawn,但光 spawn 是不够的——你还得管得住。

统一会话管理器

我写了一个 UnifiedExecManagerpackages/tools/src/tools/exec_runtime.ts),核心思路是单例 + 会话池

class UnifiedExecManager {
  private sessions = new Map<number, SessionState>()
  private nextId = 1
  private MAX_SESSIONS = 64
}

好处很明显:

  • 所有子进程都有唯一 ID
  • 随时可以查询状态、发送信号、获取输出
  • 资源回收有统一入口

资源限制:数量 + 内存 + 时间

先看数量限制:

async start(request: StartExecRequest) {
    this.cleanupSessions()
    if (this.activeSessionCount() >= MAX_SESSIONS) {
        throw new Error(`too many active sessions (max ${MAX_SESSIONS})`)
    }
    // ...
}

超过 64 个活跃会话就直接拒绝,防止被LLM恶意耗尽系统资源。

再看输出限制。Agent 交互是基于 token 计费的,子进程输出不能无限制返回:

function truncateByTokens(text: string, maxOutputTokens?: number) {
  const maxChars = (maxOutputTokens || 2000) * 4
  if (text.length <= maxChars) {
    return { output: text, deliveredChars: text.length }
  }
  return {
    output: text.slice(0, maxChars),
    deliveredChars: maxChars,
  }
}

默认最多返回 8000 字符,不够可以调,但不会无限大。

超时终止:SIGTERM → SIGKILL

子进程跑飞了是常见问题。memo 的策略是先礼貌后强硬

private async terminateForTimeout(session: SessionState) {
    if (session.exited) return
    session.proc.kill('SIGTERM')
    await waitForExit(session, 200)  // 等 200ms
    if (!session.exited) {
        session.proc.kill('SIGKILL')  // 还是没退就直接杀了
        await waitForExit(session, 200)
    }
}

为什么要等一下?因为有些程序接收到 SIGTERM 会做清理工作(比如写入缓存、关闭句柄),直接 SIGKILL 可能导致数据丢失。

内存泄漏防护:自动清理已退出的会话

会话不能只增不减。我加了一个自动清理逻辑:

private cleanupSessions() {
    if (this.sessions.size <= MAX_SESSIONS) return
    // 优先清理已退出的,按启动时间从早到晚排序
    const ended = Array.from(this.sessions.values())
        .filter(session => session.exited)
        .sort((a, b) => a.startedAtMs - b.startedAtMs)

    for (const session of ended) {
        if (this.sessions.size <= MAX_SESSIONS) break
        this.sessions.delete(session.id)
    }
}

这样即使跑了几百个命令,内存也不会无限涨。

第二道防线:命令守卫——拦截危险操作

子进程管住了还不够,还得管住跑什么命令

我见过太多「rm -rf /」惨案,也见过 dd if=/dev/zero of=/dev/sda 这种物理层面不可逆的破坏。memo 的做法是命令解析 + 黑名单匹配

命令解析:不只是字符串匹配

直接正则匹配 rm -rf 是有漏洞的。比如 sudo rm -rf /、包裹在 bash -c 里、甚至写成十六进制,都能绕过简单匹配。

memo 的做法是先把命令拆成「段」,再逐段解析:

function splitCommandSegments(command: string) {
  // 按 ; | && || 分割,处理引号和转义
  // 返回每一段独立的命令
}

function parseSegment(segment: string) {
  // 跳过 sudo/env/nohup 等包装
  // 提取真实的命令名和参数
}

这样不管外面包了多少层 sudo env bash -c,最终都能追溯到真正的命令。

危险命令黑名单

目前 memo 拦截这几类(packages/tools/src/tools/command_guard.ts):

规则 触发条件 危险等级
rm_recursive_critical_target rm -rf 目标包含 /~$HOME 等关键路径 极高
mkfs_filesystem_create mkfs/mkfs.xxx 极高
dd_write_block_device dd 写入 /dev/ 下的块设备 极高
disk_mutation_block_device fdisk/parted/shred 等操作块设备
redirect_block_device 输出重定向到 /dev/ 块设备

拦截后返回的是 <system_hint> 标记,不是直接报错,方便 Agent 理解为什么被拦:

<system_hint type="tool_call_denied"
    tool="exec_command"
    reason="dangerous_command"
    policy="blacklist"
    rule="rm_recursive_critical_target"
    command="rm -rf /">
    Blocked a high-risk shell command to prevent irreversible data loss.
    Use a safer and scoped alternative.
</system_hint>

第三道防线:审批系统——平衡权限与体验

命令守卫是第一道关卡,但还有很多「不危险但需要知道」的操作,比如写文件、改配置。审批系统的目标就是分级管理、可追溯、可配置

风险分级

memo 把工具分成三级(packages/tools/src/approval/constants.ts):

级别 含义 审批策略(auto 模式)
read 只读操作 免审批
write 文件修改 需审批
execute 执行命令 需审批

审批模式

  • auto 模式:只读工具免审批,写/执行类工具需要审批
  • strict 模式:所有工具都需要审批,一个都跑不掉
check(toolName: string, params: unknown): ApprovalCheckResult {
    if (ALWAYS_AUTO_APPROVE_TOOLS.has(toolName)) {
        return { needApproval: false, decision: 'auto-execute' }
    }

    const riskLevel = classifier.getRiskLevel(toolName)
    if (!classifier.needsApproval(riskLevel, approvalMode)) {
        return { needApproval: false, decision: 'auto-execute' }
    }
    // 生成指纹,返回需要审批
}

审批记忆:一次批准,记住一整场

如果每次执行都要点批准,用户体验会非常差。memo 用指纹 + 缓存解决这个问题:

const fingerprint = generateFingerprint(toolName, params)
cache.toolByFingerprint.set(fingerprint, toolName)

// 审批后记录
recordDecision(fingerprint, decision: 'session' | 'once' | 'deny') {
    switch (decision) {
        case 'session': cache.sessionTools.add(toolName); break
        case 'once': cache.onceTools.add(toolName); break
        case 'deny': cache.deniedTools.add(toolName); break
    }
}
  • session:这场对话内一直有效
  • once:用一次就失效
  • deny:以后再问直接拦截

dangerous 模式

审批系统是安全了,但有时候用户就是想要「无限制」——比如在本地开发、或者明确知道自己在干什么。

memo 提供了 dangerous 模式:

if (dangerous) {
  return {
    isDangerousMode: true,
    getRiskLevel: () => 'read', // 所有操作都视为最低风险
    check: () => ({ needApproval: false, decision: 'auto-execute' }),
    isGranted: () => true,
  }
}

开启也很简单,CLI 里加上 --dangerous 标记:

memo --dangerous

开启后:

  • 所有工具都免审批

这是一把双刃剑。 我在 CLI 里加了这个选项,但默认是关闭的。开发者如果想用,需要明确加上 --dangerous 标记。

总结:三层防护 + 一个后门

memo 的安全设计可以总结为:

  1. 子进程管理:数量限制 + 输出截断 + 超时终止 + 自动清理
  2. 命令守卫:命令解析 + 黑名单拦截 + stdin 检测
  3. 审批系统:风险分级 + 审批模式 + 记忆缓存
  4. dangerous 模式:留一个「我知道我在干什么」的后门

这套方案不完美,还在持续迭代。比如命令守卫目前是硬编码的黑名单,后续可以考虑支持用户自定义规则;审批系统也可以考虑接入外部信任模型。

(完)

单点登录(SSO)在前端世界的落地形态

上一章我们聊了 OAuth2 与第三方登录的三个阶段:从 Implicit Flow 的混乱时代,到 PKCE 的安全崛起,再到 OAuth 2.1 + 一键登录的无感体验。但 OAuth/OIDC 主要解决的是“授权 + 身份认证”,在企业内部多系统间实现“一次登录、处处可用”的真正 SSO 时,前端还需要面对更复杂的落地挑战:跨域、跨顶级域、微前端、浏览器隐私策略变化等。

这一篇,我们从前端视角拆解 SSO 的主流落地形态,重点对比三种核心实现方式,并讨论 2024–2026 年浏览器变化(第三方 Cookie 逐步淘汰)带来的冲击与应对。

1. SSO 在前端的核心职责与挑战(2026 年视角)

前端在 SSO 中的真实角色:

  • 检测登录状态(silent check)
  • 无感跳转 / 刷新 token
  • 跨应用同步登录/登出状态
  • 处理跨域(子域 / 不同顶级域)
  • 兼容隐私沙盒(Chrome Partitioned Cookies、Storage Partitioning)

2025–2026 年最大变化:

  • 第三方 Cookie 基本被禁用(Chrome 100% rollout)
  • Storage Partitioning(不同顶级域的 localStorage 分区)
  • iframe + postMessage 方案受限(但仍可部分工作)

因此,纯 Cookie 共享 → 纯 Token 集中 → 混合 / BFF 模式 成为主流演进路径。

2. 三种主流前端 SSO 落地方式对比(2024–2026 现状)

实现方式 适用场景 跨域支持 依赖第三方 Cookie 浏览器兼容性(2026) 安全性 复杂度 代表方案 / 协议 当前流行度
基于 Cookie 的域共享 子域 SSO(*.company.com) 子域 / 同顶级域 是(顶级域 Cookie) 高(SameSite=None+Secure) 中–高 CAS、SAML、OIDC Cookie 模式 ★★★☆☆
基于 Token 的集中式认证 跨顶级域、多 SPA、微前端 任意域 最高(无 Cookie 依赖) 中–高 OIDC + PKCE + Refresh Token ★★★★★
iframe + postMessage 通信 遗留系统、临时桥接 跨域 部分(或无) 中(分区 + 限制) 中–低 早期 CAS、Zendesk cross-storage ★☆☆☆☆

方式一:基于 Cookie 的域共享(最传统、最简单)

适用:所有应用在同一顶级域下(如 app1.company.com、app2.company.com、sso.company.com)

核心机制:

  • SSO 服务器 Set-Cookie 时设置 domain=.company.com; Secure; HttpOnly; SameSite=Lax/None
  • 浏览器在所有子域自动携带该 Cookie
  • 前端几乎无感:只需检查 Cookie 或调用 /userinfo 接口

优点:浏览器原生、无需前端代码干预、登出可直接删 Cookie

缺点:

  • 仅限子域(跨顶级域失效)
  • 第三方 Cookie 限制下需 SameSite=None; Secure + 用户许可
  • 不适合微前端 / 多顶级域场景

2026 年现状:企业内网、传统 ToB 系统仍大量使用,但新项目已转向 Token 模式。

方式二:基于 Token 的集中式认证(目前最推荐、最主流)

适用:跨顶级域、多前端(React/Vue/Next.js + 微前端)、移动 + Web 混合

核心流程(OIDC + Authorization Code + PKCE + Refresh Token):

  1. 用户访问任意前端 → 未登录 → 重定向到 SSO 中心(/authorize
  2. SSO 中心登录成功 → 返回 code → 前端(或 BFF)用 PKCE 换 token(access_token + id_token + refresh_token)
  3. 前端存储 refresh_token(HttpOnly Cookie 或 secure storage),access_token 放内存 / localStorage(短效)
  4. 所有前端共享同一 SSO 中心 → 登录一次,后续 silent renew(iframe 或 refresh token)
  5. 登出:调用 /logout + 清本地 token + 通知其他 tab(BroadcastChannel / localStorage 事件)

前端关键实现点:

  • Silent authentication:hidden iframe 打开 authorize endpoint(check session)
  • Refresh:用 refresh_token 静默换新 access_token
  • 多应用同步:BroadcastChannel 或 Service Worker 监听登录/登出事件

代表方案:

  • Auth0 / Okta / Clerk / Supabase Auth / Keycloak(OIDC 模式)
  • NextAuth / Lucia + OIDC provider
  • 自建:oidc-client-ts / @auth0/auth0-spa-js

2026 年优势:

  • 无第三方 Cookie 依赖
  • 支持跨顶级域
  • 与微前端兼容(各子应用独立管理 token,但共享 SSO 会话)

痛点:

  • 前端需处理 token 刷新、silent renew、登出广播
  • refresh_token 安全存储(推荐 BFF 或 HttpOnly Cookie)

方式三:iframe + postMessage(逐渐被淘汰的过渡方案)

早期流行于跨域 SSO(不同顶级域),典型库:cross-storage、pym.js

机制:

  • 主应用嵌入 hidden iframe 指向 SSO 域
  • iframe 内登录 → localStorage 写 token
  • postMessage 通知父窗口 → 父窗口读取

2023–2025 年后问题:

  • Storage Partitioning(Chrome 等)让跨顶级域 localStorage 隔离
  • iframe sandbox 限制 + 第三方 Cookie 禁用
  • 性能差、SEO 问题、用户体验差

2026 年现状:仅遗留系统或极特殊场景使用,新项目已弃用。

3. 微前端 / 多 SPA 下的 SSO 特殊痛点与解决方案

微前端(qiankun、Module Federation、single-spa)常见场景:

  • 不同子应用可能不同框架、不同构建
  • 需要统一登录状态

解决方案(2025–2026 推荐):

  1. 统一 SSO 中心 + Token 模式:所有子应用用同一 OIDC Client ID,共享 refresh_token(通过主应用分发或 BFF)
  2. 主应用代理登录:基座应用负责 silent check 和 token 管理,子应用通过 props / 事件总线获取状态
  3. BroadcastChannel + localStorage 事件:登录/登出时广播,子应用监听同步
  4. BFF(Backend for Frontend):每个子应用有独立 BFF,BFF 持 refresh_token,前端只拿短效 access_token

4. 2026 年 SSO 前端 Checklist(实用建议)

  • 优先选 OIDC + PKCE + Refresh Token Rotation
  • 避免依赖第三方 Cookie(除非子域 + SameSite=None)
  • 使用成熟 SDK(oidc-client-ts、@auth0/auth0-spa-js、next-auth)
  • Silent renew 用 refresh_token 而非 iframe(更可靠)
  • 登出需调用 end_session_endpoint + 清本地 + 广播
  • 高安全场景用 BFF 模式(token 永不出现在浏览器 JS)
  • 测试隐私沙盒:Chrome Incognito + 第三方 Cookie 禁用

小结 & 过渡

前端 SSO 从 Cookie 域共享 → iframe 桥接 → Token 集中式(OIDC 主导)的演进,本质上是适应浏览器隐私保护 + 跨域需求的过程。

2026 年,基于 OIDC + Refresh Token 的集中式认证 是最主流、最可靠的落地形态,尤其适合现代 Web / 微前端 / 跨域场景。

OAuth2 与第三方登录的三个阶段(2010–至今)

上一章我们聊了 Token 时代的巅峰与隐痛:双 Token、刷新机制、黑名单战争,以及各种安全加固手段。但在第三方登录(Social Login、第三方授权)领域,OAuth2 的演进路径更独立,也更戏剧化。

OAuth2 从 2010 年左右开始大规模落地,到 2025–2026 年已进入 OAuth 2.1 时代。前端在其中的角色从“被动跳转 + 解析 URL fragment”到“主动管理 PKCE + 安全刷新”,发生了翻天覆地的变化。

这一篇,我们按时间和技术范式把 OAuth2 + 第三方登录分为三个主要阶段。

1. 第一阶段:早期混乱与 Implicit Flow 主导(2010–2016 左右)

OAuth 1.0(2007–2010)太复杂,OAuth 2.0(RFC 6749,2012 年正式发布)简化了授权框架,但早期实现五花八门。

典型第三方登录流程(Google、Facebook、Twitter 等 2010–2014 年):

  • Implicit Flow(response_type=token)最流行,尤其在 SPA 和早期移动 Web
  • 前端直接发起跳转:https://accounts.google.com/o/oauth2/auth?client_id=xxx&redirect_uri=yyy&response_type=token&scope=profile email
  • 用户同意后,授权服务器重定向回 redirect_uri#access_token=xxx&expires_in=3600
  • 前端解析 URL fragment(location.hash),拿到 access_token

为什么 Implicit Flow 这么火?

  • 当时浏览器跨域限制严格(CORS 不完善,XMLHttpRequest POST 到 token endpoint 跨域困难)
  • 前端无法安全存储 client_secret(public client)
  • 简单:不用后端参与 token 交换

前端典型代码(2012–2015 年 jQuery/AngularJS 时代):

// 登录按钮点击
window.location.href = `https://accounts.google.com/o/oauth2/auth?...&response_type=token`;

// 回调页(或单页 hashchange 监听)
function handleCallback() {
  const hash = window.location.hash.substring(1);
  const params = new URLSearchParams(hash);
  const token = params.get('access_token');
  if (token) {
    localStorage.setItem('google_token', token);
    // 用 token 调用 /userinfo 或 API
  }
}

痛点与安全隐患

  • Token 暴露在 URL(浏览器历史、referer、日志、肩窥攻击)
  • 无法安全用 refresh_token(规范不推荐)
  • XSS 风险极高(token 在 JS 可读)
  • 2015–2016 年 OAuth 安全最佳实践文档开始警告 Implicit Flow

这个阶段国内微信、QQ、新浪微博登录也大量用类似“跳转 + callback 带 code/token”模式。

2. 第二阶段:Authorization Code + PKCE 的崛起与 Implicit 的逐步废弃(2016–2022 左右)

2015–2016 年,浏览器 CORS 完善 + XMLHttpRequest/Fetch 支持跨域 POST,技术条件成熟。

关键转折:

  • 2015 年:RFC 7636 PKCE(Proof Key for Code Exchange)发布,专为 public client(SPA、移动端)设计
  • 2017–2019 年:OAuth Security BCP(Best Current Practice)草案强烈推荐 Authorization Code + PKCE,视 Implicit 为 deprecated
  • 2019 年:Okta、Auth0 等大厂公开宣布“Implicit Flow 已死”
  • 2020 年后:Chrome/Firefox 等浏览器加强 URL fragment 保护 + 第三方 Cookie 限制,Implicit 更难用

现代标准流程(Authorization Code + PKCE)

  1. 前端生成 code_verifier(随机高熵字符串) + code_challenge = BASE64URL(SHA256(verifier))
  2. 跳转授权:response_type=code&code_challenge=xxx&code_challenge_method=S256
  3. 用户同意 → 重定向回 redirect_uri?code=yyy
  4. 前端(或后端代理)用 code + verifier POST 到 token endpoint 换 token

前端示例(现代 React/Vue/Next.js + oidc-client-js 或 AppAuth 库):

// 使用 @auth0/auth0-spa-js 或类似库
const auth0 = createAuth0Client({
  domain: 'xxx.auth0.com',
  clientId: 'your_client_id',
  redirectUri: window.location.origin,
  useRefreshTokens: true,  // 支持安全 refresh
});

// 登录
await auth0.loginWithRedirect({
  authorizationParams: {
    scope: 'openid profile email',
    // PKCE 自动处理
  }
});

// 回调处理(自动)
const user = await auth0.getUser();

为什么 PKCE 更好?

  • Token 从不走 URL(防泄露)
  • Code 即使被截获,攻击者无 verifier 无法换 token
  • 支持 refresh_token(带 rotation 更安全)
  • 前端角色:管理 PKCE 参数、silent refresh(iframe 或 refresh token)

这个阶段 OIDC(OpenID Connect,2014 RFC)全面普及:返回 id_token(JWT 格式身份令牌)+ access_token,前端可直接解析用户信息而无需再调 userinfo endpoint。

国内:微信/支付宝/抖音等逐步支持 PKCE 或后端代理模式。

3. 第三阶段:OAuth 2.1 时代 + 一键登录 / 无感体验(2023–至今,2026 年现状)

OAuth 2.1(draft 持续迭代,至 2025 年 10 月最新 draft-14,预计很快 RFC)正式固化最佳实践:

  • 完全移除 Implicit Flow
  • Authorization Code 强制要求 PKCE(所有 client 类型,无例外)
  • 移除 ROPC(Resource Owner Password Credentials,密码直传 grant,已废弃)
  • 强制 exact redirect_uri 匹配、更严格参数校验
  • 推荐 refresh token rotation + sender-constrained tokens

前端变化:

  • 几乎所有主流 SDK(如 Google Identity Services、Apple Sign in JS、Auth0、Clerk、Supabase Auth)默认 PKCE + OIDC
  • 一键登录普及:Google One Tap、Apple Sign in with Apple、微信一键登录(运营商取号/静默授权)
  • Popup / Redirect 混合:早期 popup 窗口常见,现在 redirect + state 参数防 CSRF 更安全
  • 移动端 / Hybrid:AppAuth-iOS/Android + WebView 统一用 Code + PKCE
  • 国内特色:手机号一键登录(本机号码识别)+ 微信/支付宝生态闭环

典型现代前端接入(2025–2026):

  • 用库处理一切:oidc-client-ts、@okta/okta-auth-js、next-auth 等
  • 支持 silent authentication(hidden iframe renew)
  • Passkey/FIDO2 作为备用(下一章无密码主题)

OAuth 2.1 影响(2025–2026 已大量落地):

  • 旧 Implicit 项目必须迁移(许多 SaaS 2024–2025 年强制下线 Implicit 支持)
  • 前端复杂度略升(需处理 PKCE),但库屏蔽了细节
  • 安全性大幅提升:token 泄露窗口缩小、可主动 revoke

小结 & 过渡

OAuth2 + 第三方登录的三个阶段总结:

阶段 时间 主导 Flow 前端角色变化 安全水平 当前状态(2026)
第一阶段 2010–2016 Implicit Flow 跳转 + 解析 URL fragment 已废弃
第二阶段 2016–2022 Auth Code + PKCE 管理 PKCE + token 刷新 中–高 主流
第三阶段 2023–至今 OAuth 2.1 强制 PKCE 一键/无感 + OIDC 身份解析 标准 & 强制趋势

OAuth2 让前端从“被动接收 token”进化到“主动、安全地管理授权流程”。但第三方登录终究是“授权”而非“认证”——真正补全身份语义的是 OpenID Connect。

❌