Repository navigation
Plugin Security
StackPanel 使用基于角色的访问控制(RBAC)。插件可以声明权限并定义角色权限模板。
插件是受信代码,不是沙盒对象。 理解这一点是用好、也是安全运营本平台的前提。
-
执行模式取决于
execution与运行环境。 manifest 的execution取'trusted' | 'isolated'。 生产环境(NODE_ENV=production)下,非 builtin 插件一律以isolated执行:内核fork()一个独立子进程,插件代码跑在子进程里,与内核不同进程、不同事件循环,能力调用经受限 RPC 往返 (RPC_MAX_IN_FLIGHT=64、RPC_MAX_BYTES=1MiB、RPC_TIMEOUT_MS=10s、RPC_MAX_RAW_STREAMS=256, 超限报错拒绝而非排队)。开发环境未声明execution时回退trusted(同进程import())。 生产环境拒绝第三方插件声明trusted。 -
隔离是「故障与资源边界」,不是「安全沙盒」。 isolated 子进程与内核同一 OS 身份、同一文件系统、
同一环境变量,仍可触达该身份能触达的一切;它提供的是内存护栏(
STACKPANEL_PLUGIN_MAX_RSS_MB默认 512,超限 exit 137;--max-old-space-size默认 256)与崩溃隔离(子进程被拆除不会带走内核), 以及 RPC 限额带来的资源上界。它不构成对抗恶意代码的安全边界。 - 无 OS 级权限边界。 无论哪种执行模式,插件都以内核进程相同的操作系统身份运行,可触达该身份 能触达的一切:文件系统、环境变量、网络、内核已连接的 PostgreSQL / Redis / 对象存储,以及内核注入的服务对象。
-
permissions不是安全边界。 manifest 的permissions、roleTemplates与路由permission字段,是HTTP 路由准入与前端可见性控制——它们约束「哪个已认证用户能调用哪条路由」,不约束插件代码本身。一旦插件被激活,它的代码即拥有内核授予该进程的全部能力。 -
注入即授信。
PluginContext暴露的能力(payments、wallet、fx、auth、notifications、jobs、state、secrets等)是内核主动授信;插件可直接调用,无需额外授权。因此插件的可信任程度等同于内核本体。 -
数据边界清晰,但非隔离。
PluginContext不提供数据库句柄(ADR-0008 §1:ctx.db已物理删除)。插件只能经 Extension 引擎(ctx.extensions/ctx.tx)读写自己声明的模型,拿不到 PrismaClient、也访问不到内核表。这是数据访问边界,用于防止插件代码无意破坏内核 schema;它不是安全沙盒——插件代码仍可通过其他途径间接影响进程。 -
签名是来源校验,不是隔离。 插件包签名验证的是来源与完整性(provenance),用于确认包来自可信发布者且未被篡改。生产环境应开启包签名强校验并配置
STACKPANEL_SIGNING_PUBLIC_KEY。签名不提供运行时隔离。
安装并激活一个插件,等价于在该服务器上以服务进程身份运行这段代码。因此:
- 只安装可信来源的插件;第三方插件应按「引入了一段任意服务器端代码」来评估风险。
- isolated 子进程降低了故障半径与资源失控风险(崩溃不带走内核、RSS 有硬顶),但不降低插件代码本身的信任要求——不可信/多租户插件仍不在当前能力范围。若未来需要对抗性隔离,须引入 Node Permission Model、容器或 VM 等 OS 级边界,这是一次架构变更,不是配置项。
- 删除插件只是停止加载其代码;已写入的数据与已声明的权限由运维自行清理。
| 角色 | 说明 |
|---|---|
ADMIN |
管理员,拥有全部权限,无需逐一授权 |
USER |
普通用户,权限由激活的插件通过 roleTemplates 动态授予 |
在 manifest 中声明插件使用的权限字符串:
{
"permissions": ["my-plugin.read", "my-plugin.write", "my-plugin.delete"]
}权限字符串是任意字符串,建议使用 <plugin-id>.<action> 格式。
roleTemplates 定义插件激活后自动授予内置角色的权限:
{
"roleTemplates": [
{ "role": "USER", "permissions": ["my-plugin.read"] },
{ "role": "ADMIN", "permissions": ["my-plugin.read", "my-plugin.write", "my-plugin.delete"] }
]
}
ADMIN始终拥有全部权限,roleTemplates 中为 ADMIN 声明的权限是冗余的(但可用于文档化)。
权限在插件激活时由 PluginRuntime.registerPluginPermissions() 写入:
- 对
permissions中每个 key:upsertPermission,并授予管理员组group_admin。 - 对每个
roleTemplates:把模板权限授予对应内置组(ADMIN→group_admin,USER→group_user)。
这是 upsert 语义,停用插件不会撤销已写入的权限(组授权是持久数据)。因此重新启用不会重复出错。
插件路由可声明 permission 字段:
routes: [
{
method: 'GET',
path: '/my-plugin/items',
auth: 'user',
permission: 'my-plugin.read',
handler: listItems,
},
{
method: 'DELETE',
path: '/my-plugin/items/:id',
auth: 'user',
permission: 'my-plugin.delete',
handler: deleteItem,
},
],Dispatcher 的 runGuards 按以下顺序检查:
1. isActive? → 否则 404
2. requireAuth → auth='user'/'admin' 或有 permission 时,需认证
3. requireRole(admin) → auth='admin' 时,需 ADMIN 角色
4. hasPermission → 有 permission 时,检查角色权限
运行时用扁平权限集合判定:request.user.permissions.has(route.permission)(request.user 由认证层解析,含用户所属组的全部权限)。auth: 'admin' 单独要求 platform.admin 权限。声明 permissions 的插件会把它们授予 group_admin,因此管理员天然通过;USER 需由 roleTemplates 显式授予。
前端 BFF 可通过 API 检查当前会话的权限:
GET /permissions/check?permission=my-plugin.read
→ { "allowed": true }
权限不足时,内核会写入审计日志 frontend.permission.denied。
管理后台可查看所有激活插件声明的权限(按插件展开为 pluginId + permission):
GET /admin/rbac/templates
→ {
"templates": [
{ "pluginId": "my-plugin", "permission": "my-plugin.read" },
{ "pluginId": "my-plugin", "permission": "my-plugin.write" }
]
}
返回的是 manifest
permissions的展开,不是roleTemplates。角色模板可在GET /admin/plugins的每项roleTemplates字段查看。
插件的 frontend adminActions 可以声明 permission 字段。渲染前,管理后台会检查当前用户是否拥有该权限,未授权的 action 不会展示。详见 管理后台扩展。
前端运行时的权限相关事件通过 POST /frontend/audit 写入审计日志:
| action | 说明 |
|---|---|
frontend.permission.denied |
权限检查未通过(由 /permissions/check 写入) |
frontend.finder.call |
Finder 数据调用 |
frontend.admin.route.render |
管理路由渲染 |
frontend.account.route.render |
账户路由渲染 |
frontend.action.execute |
前端 action 执行 |
/frontend/audit的 action 枚举为后四项;frontend.permission.denied由内核在权限检查失败时写入,不接受前端上报。
插件代表平台发起出站请求(上游 API、Webhook、回调)时,目标地址往往来自管理员
配置或远端数据。若不加校验,攻击者可把它当作访问内网/云元数据的 SSRF 原语。内核
内置 @stackpanel/net-guard 供内核与插件复用,避免各插件各写一套、口径漂移:
| 导出 | 作用 |
|---|---|
isPublicHttpUrl(url, opts?) |
协议白名单(默认仅 https:,allowHttp 放宽)、禁止内嵌凭证、主机非内网 |
isUnsafeIpAddress(address) |
判定 IPv4/IPv6 字面量是否属于保留/内网段(含 IPv4-mapped IPv6 十六进制形式) |
isUnsafeHostname(hostname) |
判定主机名是否为 localhost/内网 TLD/私有 IP |
resolvesToUnsafeAddress(host) |
异步解析 DNS,检查是否落到内网(防 DNS rebinding) |
用法:在配置写入的 zod schema 上 refine(isPublicHttpUrl(...)) 拦截,并在真正
fetch 之前再校验一次,以防御历史存量数据。生产环境务必开启(开发联调可用
NODE_ENV 等开关放行本机上游,但绝不可带入生产)。
开始
- 核心开发
构建内核扩展
- 插件
- 主题与前端包
AI 与自动化
- 内核 Agent
- MCP
对接平台
- Open Platform API
- Partner API