Skip to content
Report library
Purpose / Other

Extension Openai Skill Security Audit

What the author says it does (original text)

>-

Independent security check

Do not install or run it yet

Files checked
1
Risks found
4
Could it run dangerous commands?Looks for programs run straight after downloading, remote control of your computer, and hidden commands.No risks found
Could it expose your files or keys?Looks for uploads of files containing passwords or keys, and keys written directly in the code.Risks found: 2
Medium risk

Long-lived account billing keys are stored as raw text in application state and preserved by default

Source references: 4
What we found

The implementation puts submitted `sk-...` values directly into a canister map or single `Text` slot. It describes preservation as the default upgrade policy for almost all apps, while also stating that each key is long-lived, unscoped, and able to spend the account balance.

Why this matters

This increases both the exposure surface and retention period of the credential. If a future endpoint, upgrade, state-handling flaw, or administrative access exposes the value, the affected asset is the OpenAI account and its budget, not merely one session.

The source does store the raw key as `Text` in canister state and calls preservation across upgrades the default. This extends the retention of a powerful, long-lived billing credential; compromise of canister state, upgrades, or dependencies could affect the whole OpenAI account. The skill also forbids getters, logging, and frontend readback, so the evidence does not show ordinary users can directly retrieve keys. Users can ask about at-rest protection, rotation, and deletion policies.

SKILL.md:99In the instructionsOpen original file
- 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.
Show 3 other places
SKILL.md:147In the instructionsOpen original file
  // Per-user OpenAI keys. Never iterated except by the calling principal.  let openAIKeys : Map.Map<Principal, Text>;  include MixinOpenAIChat(openAIKeys);};
SKILL.md:187In the instructionsOpen original file
  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:262In the instructionsOpen original file
- **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.
Medium risk

Chat input is sent to OpenAI without a required end-user disclosure or sensitive-data restriction

Source references: 2
What we found

The backend places the full `prompt` into a user message and invokes the OpenAI Chat API. The frontend instructions describe only a text area and message list; they do not require a privacy notice, sensitive-data filtering, or user consent.

Why this matters

If a user pastes personal data, business secrets, credentials, or regulated information, that content is processed by an external model service and may violate user expectations or organizational data policy.

The implementation submits the user's complete `prompt` to OpenAI as a message. The complete source's frontend requirements describe only a textarea, submission, and message list, without requiring notice of third-party processing, consent, or sensitive-data filtering. Consequently, personal data, trade secrets, or credentials entered by a user would leave the app and be sent to OpenAI. Users should ask for clear disclosure, sensitive-content controls, and a retention policy.

SKILL.md:231In the instructionsOpen original file
  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);
Show 1 other places
SKILL.md:491In the instructionsOpen original file
- 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.
Could it delete files or keep running?Looks for broad file deletion, disk overwrites, and programs set to start automatically.No risks found
Could it bypass safety checks?Looks for skipped website security checks, excessive file access, or actions that skip your approval.No risks found
Could it mislead the AI or hide text?Checks the skill instructions for requests to ignore you, influence the report, or hide text in invisible characters.Risks found: 1
Medium risk

Broad mandatory triggers can lock a project to a third-party package and architecture after a mere LLM mention

Source references: 4
What we found

The Skill says it must load whenever any user, specification, or prior task mentions GPT, an LLM, a chatbot, or embeddings. It declares `openai-client` the only permitted BYOK path and tells the agent to propagate that choice to backend agents. It then directs an installation command that rewrites dependencies and the lockfile.

Why this matters

Even when the user is only discussing a concept, needs an unsupported OpenAI API, or wants alternatives evaluated, an agent may bind the project to a specific package, authentication design, and version. This changes technical decisions and the project's supply-chain trust.

The mandatory routing described by the candidate is present: even a mention of broad LLM terms by a user, specification, or prior task triggers the skill, and relevant build specs must pin `openai-client` and propagate that choice to backend agents. Its install command changes both dependency and lock files. Although later text narrows the intended scope to OpenAI BYOK in Caffeine apps, the broad trigger could still affect discussions, other providers, or undecided architectures. Users can restrict it to explicitly authorized Caffeine OpenAI BYOK builds.

SKILL.md:4In the instructionsOpen original file
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
Show 3 other places
SKILL.md:45In the instructionsOpen original file
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:73In the instructionsOpen original file
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:28In the instructionsOpen original file
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-...`.
Could it change links or payment recipients without asking?Looks for forced referral or payment changes combined with instructions to hide the change.Risks found: 1
High risk

Shared-key variants let broad callers spend the billed account, and anonymous visitors can replace the key

Source references: 6
What we found

The admin variant lets every caller with `#user` permission use the operator's global key, while the example has no request rate, quota, or cost ceiling. The anonymous variant also removes the chat permission check and lets any visitor overwrite the current key. The Skill states that the key spends the account balance and is rate-limited per key.

Why this matters

A user or automated client could repeatedly invoke the model, causing unexpected OpenAI charges and rate-limit exhaustion. An anonymous visitor could also replace a working key, disrupting service or switching it to an account the operator did not approve.

This risk applies when the operator-funded or fully anonymous variant is deployed. The admin variant lets every caller with `#user` permission use one billing key; the anonymous variant explicitly removes the chat permission check and lets any visitor replace the key. Because the key is long-lived and billing-bound, a broadly accessible deployment could incur unexpected charges. Users should ask about quotas, rate limits, and protection against key replacement.

SKILL.md:99In the instructionsOpen original file
- 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.
Show 5 other places
SKILL.md:372In the instructionsOpen original file
  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:391In the instructionsOpen original file
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:417In the instructionsOpen original file
  ```  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:418In the instructionsOpen original file
  ```  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:425In the instructionsOpen original file
### 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.

Inside this skill

2 instruction sections

The Skill requires Caffeine apps to call OpenAI through `openai-client@0.2.5`; its installation command modifies the dependency manifest and lockfile.

View source
SKILL.md:62In the instructionsOpen original file
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:73In the instructionsOpen original file
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`.

The default variant accepts each signed-in user's OpenAI key, stores it under the caller's Principal, and retrieves it to make chat requests. It does not expose a key-returning frontend endpoint.

View source
SKILL.md:182In the instructionsOpen original file
// 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);  };

There are two shared-key variants: the admin variant lets only an admin replace the key but allows authorized users to chat with it; the anonymous variant removes these permission checks so any visitor can replace the key and invoke chat.

View source
SKILL.md:365In the instructionsOpen original file
  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:415In the instructionsOpen original file
  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.

Chat text and the bearer are used for an OpenAI Chat API request. The configuration explicitly disables replicated outcalls to avoid duplicating that request across subnet nodes.

View source
SKILL.md:223In the instructionsOpen original file
  // 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);
Start here · InstructionsSKILL.md
extension-openai
Lines connect the instruction file to its sections, not an observed execution order. Select a section to read the source.
Files and check records1 files

Coverage and gaps

Content covered in each file

These are the source ranges included in this check, not a guarantee that every issue has been resolved.

  • SKILL.mdFull text included

This report is for the version above. We read the available code and instructions without running the skill or checking extra packages it installs. This is not a promise of safety: a different version or setup may behave differently.

  • SKILL.mdInstructions

Operations mentioned in code and instructions

Read keys or account settings
SKILL.md:6In the instructionsOpen original file
  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:30In the instructionsOpen original file
[`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:83In the instructionsOpen original file
## 2. Auth model — API-key bearer, not OAuth
Connect to websites
SKILL.md:24In the instructionsOpen original file
# OpenAI integrationOpenAI / LLM extension for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral).
SKILL.md:38In the instructionsOpen original file
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:85In the instructionsOpen original file
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.
Run commands
SKILL.md:75In the instructionsOpen original file
```bashmops add openai-client@0.2.5
Lines read
503
File checksum (to compare versions)
867ec5938762a0b6080c92b62e9b0a7affa57f4960a5d0aaeb525c169f0f3904