跳转到正文
报告库
用途分类 / 其他用途

Extension Openai Skill 安全审计

作者说它能做什么(原文)

>-

第三方安全检查结论

先别安装或运行

已检查文件
1
发现的风险
4
会不会运行危险命令?检查是否下载程序后直接运行、让他人远程控制电脑,或藏起要运行的命令。未发现风险
会不会泄露文件和密钥?检查是否发送含密码或密钥的文件,以及代码里是否直接写了密钥。发现 2 项风险
中风险

长期、全账户计费密钥被以原始文本保存在应用状态中,并默认跨升级保留

原文依据:4 处
发现了什么

实现把用户提交的 `sk-...` 直接作为 `Text` 放入 canister 映射或单一变量。说明将“保留”列为几乎所有应用的默认升级策略,同时指出该密钥长期有效、无权限范围且可消耗整个账户余额。

为什么需要注意

密钥在应用中的暴露面和保存时间被扩大。如果未来的端点、升级、状态处理或管理访问泄露这些值,受影响的不只是一次会话,而是相应 OpenAI 账户及其预算。

源码确实要求把原始密钥作为 `Text` 存入 canister 状态,并称跨升级保留是默认选择。这会延长高权限、长期计费凭据的保存期;若 canister 状态、升级流程或依赖出现问题,影响可能涉及整个 OpenAI 账户。不过技能也明确禁止 getter、日志和前端回读,因此证据不表明普通用户能够直接读取密钥。用户可要求说明静态存储保护、轮换和删除策略。

SKILL.md:99来自说明文档打开原文件
- Long-lived, no expiry. Spends the entire OpenAI account balance on every call.- No scoped permissions — there is no "tweet.read"-style narrowing. Every key has full account access.- OpenAI rate-limits per-key per-minute; treat the key like a billing credential, not a session token.- **Never returned by any `query` or `shared` function.** Never logged. Never sent to the frontend. Never put in a stable variable that another endpoint with a weaker gate could read.
查看另外 3 个位置
SKILL.md:147来自说明文档打开原文件
  // Per-user OpenAI keys. Never iterated except by the calling principal.  let openAIKeys : Map.Map<Principal, Text>;  include MixinOpenAIChat(openAIKeys);};
SKILL.md:187来自说明文档打开原文件
  public shared ({ caller }) func setMyOpenAIApiKey(key : Text) : async () {    if (caller.isAnonymous()) {      Runtime.trap("Sign in to use this feature");    };    openAIKeys.add(caller, key);  };
SKILL.md:262来自说明文档打开原文件
- **Anonymous callers must not store keys.** `caller.isAnonymous()` short-circuits before any `openAIKeys.add` — otherwise everyone reading the canister via `2vxsx-fae` shares one key slot.- **`stable var` / migration.** The `Map<Principal, Text>` lives in stable memory like any other actor field; on upgrade, decide whether to preserve, rotate, or drop the keys. The default (preserve) is correct for almost all apps. If you ever rotate, drop the whole map — never partially.
中风险

聊天输入被发送给 OpenAI,但技能没有要求向最终用户说明或限制敏感内容

原文依据:2 处
发现了什么

后端把完整的 `prompt` 放入用户消息,并调用 OpenAI Chat API。前端说明只描述输入框和消息列表,没有要求隐私提示、敏感数据过滤或用户同意。

为什么需要注意

用户若粘贴个人信息、商业机密、凭据或受监管数据,这些内容会交给外部模型服务处理,可能违反用户预期或组织的数据政策。

实现会把用户输入的完整 `prompt` 作为消息提交给 OpenAI。完整源码的前端要求只描述文本框、提交和消息列表,未要求在发送前提示第三方处理、征得同意或过滤敏感数据。因此,若用户输入个人资料、商业秘密或凭据,这些内容会离开应用并交给 OpenAI。用户应要求明确的数据提示、敏感内容限制及保留政策。

SKILL.md:231来自说明文档打开原文件
  public func runChatCompletion(config : Config, prompt : Text) : async* Text {    let userMessage = ChatCompletionRequestUserMessage.JSON.init({      content = #string(prompt);      role = #user;    });    // `JSON.init` defaults every optional to `null` — DO NOT hand-list them.    // Layer optionals with record-update syntax:    //   { CreateChatCompletionRequest.JSON.init {...} with temperature = ?0.7 }    let req = CreateChatCompletionRequest.JSON.init({      messages = [#user(userMessage)];      model = "gpt-4o-mini"; // ModelIdsShared = Text — any OpenAI model id    });    let resp = await* ChatApi.createChatCompletion(config, req);
查看另外 1 个位置
SKILL.md:491来自说明文档打开原文件
- The chat UI itself is trivial and identical across variants: a textarea, a submit button, a list of messages bound to the backend's chat endpoint. No client-side OpenAI SDK, no key handling, no streaming-protocol logic — the canister mediates everything.- **Sign-in is required for variants A and B, skipped for variant C.** For A and B, wire the chat and settings routes through `extension-authorization`'s auth guard (`useInternetIdentity` + a redirect when `!isAuthenticated`); anonymous callers must hit a "please sign in" wall before the chat or settings UI renders, otherwise every backend call traps. For C, no guard is needed because there is no auth model.- The frontend never persists the key in localStorage / IndexedDB / cookies. It travels into the canister via the typed setter and is never read back.
会不会删除文件或一直在后台运行?检查是否大范围删除文件、改写磁盘,或设置自动启动。未发现风险
会不会绕过安全保护?检查是否跳过网站安全验证、开放过多文件权限,或取消操作前的确认。未发现风险
会不会误导 AI 或隐藏内容?检查工作说明是否要求 AI 忽略你的指令、干扰检查结果,或夹带看不见的文字。发现 1 项风险
中风险

宽泛的“强制”触发规则可在仅提及 LLM 时锁定第三方包和架构选择

原文依据:4 处
发现了什么

技能宣称只要用户、规格或先前任务提到任何 GPT、LLM、聊天机器人或嵌入,就必须加载该技能,并将 `openai-client` 定为唯一允许的 BYOK 路径,还要求把这一选择传播给后端代理。随后它指示执行会改写依赖及锁文件的安装命令。

为什么需要注意

即使用户只是讨论概念、需要不同 OpenAI API,或希望评估替代方案,代理也可能把应用绑定到特定包、认证设计和版本,并修改项目依赖。这会影响技术选择及后续供应链信任。

候选描述的强制路由确实存在:即使只是用户、规格或先前任务提及广泛的 LLM 术语,技能也要求加载自身,并要求相关构建规格固定使用 `openai-client`、向后端代理传播该选择。安装命令会修改依赖清单和锁文件。虽然正文后来把适用范围缩到 Caffeine 应用中的 OpenAI BYOK,这种宽泛触发仍可能影响原本只讨论 LLM、使用其他供应商或尚未决定架构的任务。用户可限制其仅用于明确授权的 Caffeine OpenAI BYOK 构建。

SKILL.md:4来自说明文档打开原文件
description: >-  MANDATORY recipe for every Caffeine build that calls OpenAI (ChatGPT,  GPT-4o, an LLM, a chatbot, embeddings). The ONLY supported path is the  `openai-client` mops package with a canister-side API-key bearer.  Hand-rolling `ic.http_request` to `api.openai.com/v1/...` is a  FORBIDDEN anti-pattern — it leaks the bearer across replicated  outcalls (security + 13× billing impact), bypasses the typed  request/response bindings, and forces hand-rolled JSON on a language  with poor JSON support. Load this skill whenever the user, spec, or  any prior task mentions ChatGPT, GPT (any version), OpenAI, an LLM, a  chatbot, or embeddings — and BEFORE writing any code that touches  `api.openai.com`.version: 0.1.3
查看另外 3 个位置
SKILL.md:45来自说明文档打开原文件
hand-rolled JSON serialisation on a language with weak JSON support.Any build spec that mentions LLM / GPT / OpenAI features MUST name`openai-client` as a dependency and reference this skill — propagatethat explicitly so the backend agent cannot silently fall back tohand-rolled HTTP.
SKILL.md:73来自说明文档打开原文件
Use the mops tool, not manual file edits:```bashmops add openai-client@0.2.5```This updates `mops.toml` (adds `openai-client = "0.2.5"` to `[dependencies]`) and rewrites `mops.lock` in one step. **Requires Mops ≥ 2.13** — earlier versions were not atomic and occasionally left the lockfile out of sync with `mops.toml`.
SKILL.md:28来自说明文档打开原文件
For an LLM **inside a Caffeine app** with no user-pasted OpenAI key, use[`extension-inference`](../extension-inference/SKILL.md)(`caffeineai-inference-client`, credentials provided by the platform). Thisskill is **only** for calling `api.openai.com` with a user- or admin-pasted`sk-...`.
会不会偷偷改推广链接或收款方?检查是否强制替换推广链接或收款对象,同时要求隐瞒更改。发现 1 项风险
高风险

共享密钥方案允许广泛调用者消耗计费账户,且匿名访客还能替换密钥

原文依据:6 处
发现了什么

管理员方案让任何拥有 `#user` 权限的人使用运营者的全局密钥;示例没有调用频率、额度或成本上限。匿名方案进一步移除聊天权限检查,并允许任何访客覆盖当前密钥。技能自己说明密钥会消耗账户余额且按密钥限流。

为什么需要注意

用户或自动化程序可反复调用模型,造成意外 OpenAI 费用和限流。匿名访客还可覆盖有效密钥,使服务中断或改用未经运营者批准的账户。

该风险在采用运营者付费或完全匿名方案时成立。管理员方案让所有具备 `#user` 权限的人使用同一个计费密钥;匿名方案还明确删除聊天权限检查,并允许任何访客替换密钥。由于密钥长期有效并关联账户支出,公开或用户范围过宽的部署可能造成未预期费用。用户应要求作者说明调用配额、速率限制和密钥替换保护。

SKILL.md:99来自说明文档打开原文件
- Long-lived, no expiry. Spends the entire OpenAI account balance on every call.- No scoped permissions — there is no "tweet.read"-style narrowing. Every key has full account access.- OpenAI rate-limits per-key per-minute; treat the key like a billing credential, not a session token.- **Never returned by any `query` or `shared` function.** Never logged. Never sent to the frontend. Never put in a stable variable that another endpoint with a weaker gate could read.
查看另外 5 个位置
SKILL.md:372来自说明文档打开原文件
  public shared ({ caller }) func chat(prompt : Text) : async Text {    if (not AccessControl.hasPermission(accessControlState, caller, #user)) {      Runtime.trap("Unauthorized");    };    let ?key = openAIApiKey.value else Runtime.trap("OpenAI is not configured");    await* OpenAI.runChatCompletion(OpenAI.configForKey(key), prompt);  };
SKILL.md:391来自说明文档打开原文件
Use this **only** when the spec explicitly states there is no login at all (single-user demo, intra-team tool, throwaway sandbox). Mechanically identical to §9 — single `?Text` key, no getter, `isOpenAIConfigured` query — but with the auth import / `#admin` gate removed; any visitor may overwrite the key.
SKILL.md:417来自说明文档打开原文件
  ```  public func setOpenAIApiKey(key : Text) : async () {    openAIApiKey.value := ?key;  };  ```- Drop the `#user` permission check at the top of `chat`. `chat`, `isOpenAIConfigured`, and the `OpenAI.configForKey(...)` call are otherwise identical to §9.
SKILL.md:418来自说明文档打开原文件
  ```  public func setOpenAIApiKey(key : Text) : async () {    openAIApiKey.value := ?key;  };  ```- Drop the `#user` permission check at the top of `chat`. `chat`, `isOpenAIConfigured`, and the `OpenAI.configForKey(...)` call are otherwise identical to §9.
SKILL.md:425来自说明文档打开原文件
### Anonymous-specific invariants- **No `extension-authorization` import.** This variant skips it entirely.- **The key is shared and replaceable by anyone.** That is the explicit trade-off of the variant; pick it only when the spec accepts that.- **Same no-getter / no-log rules apply.** `openAIApiKey` is read only inside `chat` (then passed to `OpenAI.configForKey`), never returned by any endpoint.- **Build a fresh `Config` per call** — same reasoning as §9.

Skill 逻辑拆解

2 个说明模块

该技能要求 Caffeine 应用通过 `openai-client@0.2.5` 调用 OpenAI,并让安装命令修改依赖清单和锁文件。

查看原文
SKILL.md:62来自说明文档打开原文件
1. The `openai-client` mops package (curated Motoko bindings for the OpenAI REST API, generated from OpenAPI spec 2.3.0).2. A way to store the OpenAI API key (`sk-...`) as a canister-side secret. Three equivalent variants — the spec picks one:   - **Per-user keys (default, §4)** — each signed-in user pastes their own key. Each user funds their own usage. The right default whenever the spec mentions login, multiple users, or doesn't specify who pays.   - **Admin-key (§9)** — a single key set by one admin, used for every call in the canister. Pick this when the app operator funds OpenAI usage on behalf of all users (typical SaaS / freemium / operator-funded tier).   - **Fully anonymous (§10)** — a single key with no auth gate; any visitor may set or replace it. Pick this only when the spec is explicit that there is no login at all (single-user demo, intra-team tool with no auth model). Same backend shape as §9 minus the `#admin` permission check.3. A `Config` value that pins `is_replicated = ?false` — non-negotiable, see §3.
SKILL.md:73来自说明文档打开原文件
Use the mops tool, not manual file edits:```bashmops add openai-client@0.2.5```This updates `mops.toml` (adds `openai-client = "0.2.5"` to `[dependencies]`) and rewrites `mops.lock` in one step. **Requires Mops ≥ 2.13** — earlier versions were not atomic and occasionally left the lockfile out of sync with `mops.toml`.

默认方案接收每位登录用户的 OpenAI 密钥,以调用者 Principal 为索引长期存储;聊天时读取该密钥并用它发送请求。没有向前端返回密钥的接口。

查看原文
SKILL.md:182来自说明文档打开原文件
// Pairs with `MixinAuthorization` to gate every endpoint on a signed-in caller.mixin (openAIKeys : Map.Map<Principal, Text>) {  public query ({ caller }) func isMyOpenAIConfigured() : async Bool {    openAIKeys.containsKey(caller);  };  public shared ({ caller }) func setMyOpenAIApiKey(key : Text) : async () {    if (caller.isAnonymous()) {      Runtime.trap("Sign in to use this feature");    };    openAIKeys.add(caller, key);  };  public shared ({ caller }) func clearMyOpenAIApiKey() : async () {    if (caller.isAnonymous()) {      Runtime.trap("Sign in to use this feature");    };    openAIKeys.remove(caller);  };  public shared ({ caller }) func chat(prompt : Text) : async Text {    if (caller.isAnonymous()) {      Runtime.trap("Sign in to use this feature");    };    let ?key = openAIKeys.get(caller) else {      Runtime.trap("Set your OpenAI API key first");    };    await* OpenAI.runChatCompletion(OpenAI.configForKey(key), prompt);  };

共享密钥有两种方案:管理员方案仅允许管理员更换密钥,但普通授权用户可用它聊天;匿名方案移除这些权限检查,任何访客都能更换密钥并调用聊天。

查看原文
SKILL.md:365来自说明文档打开原文件
  public shared ({ caller }) func setOpenAIApiKey(key : Text) : async () {    if (not AccessControl.hasPermission(accessControlState, caller, #admin)) {      Runtime.trap("Unauthorized: Only admins can set the OpenAI API key");    };    openAIApiKey.value := ?key;  };  public shared ({ caller }) func chat(prompt : Text) : async Text {    if (not AccessControl.hasPermission(accessControlState, caller, #user)) {      Runtime.trap("Unauthorized");    };    let ?key = openAIApiKey.value else Runtime.trap("OpenAI is not configured");    await* OpenAI.runChatCompletion(OpenAI.configForKey(key), prompt);  };
SKILL.md:415来自说明文档打开原文件
  with the unauthenticated form:  ```  public func setOpenAIApiKey(key : Text) : async () {    openAIApiKey.value := ?key;  };  ```- Drop the `#user` permission check at the top of `chat`. `chat`, `isOpenAIConfigured`, and the `OpenAI.configForKey(...)` call are otherwise identical to §9.

聊天内容会与密钥一起用于 OpenAI Chat API 请求;配置明确关闭复制式外呼,以避免同一请求被子网节点重复发送。

查看原文
SKILL.md:223来自说明文档打开原文件
  // REQUIRED — see §3: security, billing, and non-determinism all force it.  public func configForKey(key : Text) : Config {    {      defaultConfig with      auth = ?#bearer key;      is_replicated = ?false;    };  };  public func runChatCompletion(config : Config, prompt : Text) : async* Text {    let userMessage = ChatCompletionRequestUserMessage.JSON.init({      content = #string(prompt);      role = #user;    });    // `JSON.init` defaults every optional to `null` — DO NOT hand-list them.    // Layer optionals with record-update syntax:    //   { CreateChatCompletionRequest.JSON.init {...} with temperature = ?0.7 }    let req = CreateChatCompletionRequest.JSON.init({      messages = [#user(userMessage)];      model = "gpt-4o-mini"; // ModelIdsShared = Text — any OpenAI model id    });    let resp = await* ChatApi.createChatCompletion(config, req);
从这里开始 · 工作说明SKILL.md
extension-openai
连线表示工作说明包含的模块,不是实际运行顺序。点击模块可查看原文。
文件与检查记录1 个文件

检查范围与遗漏

逐文件查看涉及的内容

下方列出本次涉及的原文范围;纳入检查不代表已查清所有问题。

  • SKILL.md已纳入全文

这份报告只针对上方版本。我们看了拿到的代码和说明文件,没有实际运行 Skill,也没有检查它另外安装的软件包。因此,这不是“保证安全”的承诺;换了版本或使用环境,结果也可能不同。

  • SKILL.md工作说明

代码和说明中提到的操作

读取密钥或账号配置
SKILL.md:6来自说明文档打开原文件
  GPT-4o, an LLM, a chatbot, embeddings). The ONLY supported path is the  `openai-client` mops package with a canister-side API-key bearer.  Hand-rolling `ic.http_request` to `api.openai.com/v1/...` is a
SKILL.md:30来自说明文档打开原文件
[`extension-inference`](../extension-inference/SKILL.md)(`caffeineai-inference-client`, credentials provided by the platform). Thisskill is **only** for calling `api.openai.com` with a user- or admin-pasted
SKILL.md:83来自说明文档打开原文件
## 2. Auth model — API-key bearer, not OAuth
连接外部网站
SKILL.md:24来自说明文档打开原文件
# OpenAI integrationOpenAI / LLM extension for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral).
SKILL.md:38来自说明文档打开原文件
GPT", "summarise with an LLM", "build a chatbot", or "generateembeddings" requests. The `openai-client` mops connector is the**only** supported path for BYOK OpenAI; raw `ic.http_request` to
SKILL.md:85来自说明文档打开原文件
Unlike X / Twitter, OpenAI uses a **single static bearer per account**: an `sk-...` key issued from [platform.openai.com/api-keys](https://platform.openai.com/api-keys). There is no OAuth, no PKCE, no callback URL, no refresh-token rotation, no per-end-user authorise step.
运行命令
SKILL.md:75来自说明文档打开原文件
```bashmops add openai-client@0.2.5
读取了多少行
503
文件校验值(用于核对版本)
867ec5938762a0b6080c92b62e9b0a7affa57f4960a5d0aaeb525c169f0f3904