| Título | CherryHQ cherry-studio 1.9.6 Authorization Bypass / Flow-Key Confusion |
|---|
| Descripción | ## Vulnerability Title
Authorization Bypass via Hash-Key Confusion in CherryIN OAuth Flow Binding
## Affected Component
`src/main/services/CherryINOAuthService.ts` and `src/preload/index.ts`
Repository: https://github.com/CherryHQ/cherry-studio
## Summary
An attacker with the ability to execute JavaScript in a Cherry Studio renderer that can access the CherryIN preload API and observe a valid OAuth callback can craft a security-distinct token exchange request that reuses another renderer's OAuth `state` key. This causes flow-key confusion, which leads to cross-renderer OAuth flow consumption and possible CherryIN API key disclosure.
## Technical Details
The vulnerability occurs because the CherryIN OAuth service treats the OAuth `state` string as the sole identity for pending OAuth flows. The `state` value is used as an application-level key for retrieving the stored PKCE verifier and OAuth server configuration, but the key does not include the initiating IPC sender or window.
**Where the Flow Key is Computed**
In `src/main/services/CherryINOAuthService.ts`, pending flows are tracked globally in a `Map` keyed only by the `state` string:
```ts
const pendingOAuthFlows = new Map<string, PendingOAuthFlow>()
```
When `startOAuthFlow` is called, the PKCE `codeVerifier` and a random 32-character `state` are mapped together, omitting any tracking of `event.sender` or window contexts:
```ts
pendingOAuthFlows.set(state, {
codeVerifier,
oauthServer,
apiHost: resolvedApiHost,
timestamp: Date.now()
})
```
**Vulnerable Token Exchange Lookup**
When exchanging the authorization code, the IPC handler checks `pendingOAuthFlows` using only the client-provided `state` parameter:
```ts
public exchangeToken = async (
_: Electron.IpcMainInvokeEvent,
code: string,
state: string
): Promise<TokenExchangeResult> => {
const flowData = pendingOAuthFlows.get(state)
if (!flowData) {
throw new CherryINOAuthServiceError('OAuth flow expired or not found')
}
pendingOAuthFlows.delete(state)
// ... proceed to exchange token and return API keys to caller ...
```
Because the `IpcMainInvokeEvent` sender context (`_`) is ignored, any context within the Electron application that can trigger the exposed `cherryin:exchangeToken` preload IPC channel can intercept and consume a valid `state` lookup transaction initiated by a different window.
## Impact
This vulnerability allows attackers to:
- Complete or consume a CherryIN OAuth flow initiated by another renderer or window context.
- Exfiltrate CherryIN API keys returned from the cross-sender `exchangeToken` handler response.
- Cause a localized Denial of Service (DoS) by deleting and consuming the transient pending state before the legitimate initiator completes their flow.
## Proof of Concept
The following Node.js test environment models the main-process mapping logic to demonstrate how a mismatching IPC sender can pull flow data and exfiltrate keys based on `state` tracking alone.
```js
#!/usr/bin/env node
const pendingOAuthFlows = new Map()
async function startOAuthFlow(event, state) {
pendingOAuthFlows.set(state, {
codeVerifier: 'victim_pkce_verifier',
oauthServer: '[https://open.cherryin.ai](https://open.cherryin.ai)',
apiHost: '[https://open.cherryin.ai](https://open.cherryin.ai)',
timestamp: Date.now()
})
return { state, sender: event.sender.id }
}
async function exchangeToken(event, code, state) {
const flowData = pendingOAuthFlows.get(state)
if (!flowData) {
throw new Error('OAuth flow expired or not found')
}
pendingOAuthFlows.delete(state)
return {
exchangedBySender: event.sender.id,
code,
usedCodeVerifier: flowData.codeVerifier,
apiKeys: 'VICTIM_CHERRYIN_API_KEY'
}
}
async function main() {
const victimEvent = { sender: { id: 17, label: 'victim window' } }
const attackerEvent = { sender: { id: 42, label: 'attacker renderer' } }
const sharedState = 'NsXVzSt3Aiz5CchrVGUGY7OI0s5lPLKe'
await startOAuthFlow(victimEvent, sharedState)
const attackerResult = await exchangeToken(attackerEvent, 'AUTH_CODE_FOR_VICTIM', sharedState)
console.log(JSON.stringify({
victimStartedFlowId: victimEvent.sender.id,
attackerExchangedFlowId: attackerEvent.sender.id,
attackerExfiltrationResult: attackerResult,
vulnerable: attackerResult.exchangedBySender === 42
}, null, 2))
}
main()
```
*Observed execution output: `"vulnerable": true`, confirming that a separate renderer context can pull down the active state record and intercept the token payloads.*
## Remediation
Explicitly bind each pending CherryIN OAuth flow sequence to the originating IPC sender/window context.
1. **Verify IPC Sender Context**: Store `event.sender.id` or the unique `BrowserWindow` identifier within the `PendingOAuthFlow` structure during `startOAuthFlow`.
2. **Enforce Authorization Bounds**: Modify `exchangeToken` to validate that the caller's sender ID matches the recorded initiator before resolving the lookup:
```ts
if (flowData.initiatingSenderId !== event.sender.id) {
throw new CherryINOAuthServiceError('Unauthorized flow context invocation')
}
```
3. **Capability Tokens**: Alternatively, pass an unforgeable token unique to the initiating renderer frame that must be presented alongside the authorization code during the extraction workflow.
## References
- Core OAuth Service state mapping logic: `src/main/services/CherryINOAuthService.ts`
- Preload IPC interface exposure bindings: `src/preload/index.ts`
- Global IPC channel registration definitions: `src/main/ipc.ts` |
|---|
| Fuente | ⚠️ https://github.com/CherryHQ/cherry-studio/issues/15411 |
|---|
| Usuario | dem0000 (UID 98390) |
|---|
| Sumisión | 2026-05-29 03:17 (hace 1 mes) |
|---|
| Moderación | 2026-06-28 11:26 (1 month later) |
|---|
| Estado | Aceptado |
|---|
| Entrada de VulDB | 374542 [CherryHQ cherry-studio hasta 1.9.7 CherryIN Preload API MemoryService.ts sha256 state escalada de privilegios] |
|---|
| Puntos | 20 |
|---|