mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-19 00:14:40 +00:00
fix: stop leaking connector OAuth session tokens to other origins
The connector OAuth popup posted the session token to window.opener with a '*' target origin, so any page that opened the popup received it. An attacker with an account on a multi-user deployment could start a flow for their own pending session, get a victim to finish the provider consent, and receive a token backed by the victim's Drive/SharePoint/Confluence tokens. - Post popup results only to allowed frontend origins: the callback origin, OIDC_FRONTEND_URL, the new CONNECTOR_ALLOWED_ORIGINS, and localhost:5173 when the callback runs on a loopback host. - Render the success page from the callback itself so the token never appears in a URL; callback-status ignores session_token/user_email params. - ConnectorAuth accepts messages only from the popup it opened, on the callback origin reported by /api/connectors/auth. - /api/connectors/disconnect requires auth and only deletes the caller's session. - /api/connectors/sync and /api/remote reject session tokens the caller does not own. Fixes #2766
This commit is contained in:
1 parent
d121eec2f5
commit
41b3afed14
14 files changed
+575
-87
No files matched your search
@@ -5,6 +5,7 @@ import { useSelector } from 'react-redux';
|
||||
import userService from '../api/services/userService';
|
||||
import { useDarkTheme } from '../hooks';
|
||||
import { selectToken } from '../preferences/preferenceSlice';
|
||||
import { isTrustedConnectorMessage } from '../utils/connectorAuthUtils';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
interface ConnectorAuthProps {
|
||||
@@ -34,6 +35,8 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
const completedRef = useRef(false);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
const authWindowRef = useRef<Window | null>(null);
|
||||
// Origin the OAuth callback page is served from, as reported by the backend.
|
||||
const callbackOriginRef = useRef<string | null>(null);
|
||||
// Hold the exact listener identity so unmount cleanup removes the same fn.
|
||||
const messageHandlerRef = useRef<((event: MessageEvent) => void) | null>(
|
||||
null,
|
||||
@@ -54,6 +57,16 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
};
|
||||
|
||||
const handleAuthMessage = (event: MessageEvent) => {
|
||||
// Only the popup we opened, on the callback origin, may report a result.
|
||||
if (
|
||||
!isTrustedConnectorMessage(
|
||||
event,
|
||||
authWindowRef.current,
|
||||
callbackOriginRef.current,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const successGeneric = event.data?.type === 'connector_auth_success';
|
||||
const successProvider = event.data?.type === `${provider}_auth_success`;
|
||||
const errorProvider = event.data?.type === `${provider}_auth_error`;
|
||||
@@ -139,6 +152,7 @@ const ConnectorAuth: React.FC<ConnectorAuthProps> = ({
|
||||
onError(t('modals.uploadDoc.connectors.auth.authCancelled'));
|
||||
return;
|
||||
}
|
||||
callbackOriginRef.current = authData.callback_origin || null;
|
||||
authWindow.location.href = authData.authorization_url;
|
||||
|
||||
messageHandlerRef.current = handleAuthMessage;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isTrustedConnectorMessage } from './connectorAuthUtils';
|
||||
|
||||
const popup = {} as Window;
|
||||
const otherWindow = {} as Window;
|
||||
|
||||
describe('isTrustedConnectorMessage', () => {
|
||||
it('rejects messages when no auth popup is open', () => {
|
||||
expect(
|
||||
isTrustedConnectorMessage(
|
||||
{ source: popup, origin: 'https://api.example.com' },
|
||||
null,
|
||||
'https://api.example.com',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects messages that do not come from the auth popup', () => {
|
||||
expect(
|
||||
isTrustedConnectorMessage(
|
||||
{ source: otherWindow, origin: 'https://api.example.com' },
|
||||
popup,
|
||||
'https://api.example.com',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects messages from the popup when its origin is unexpected', () => {
|
||||
expect(
|
||||
isTrustedConnectorMessage(
|
||||
{ source: popup, origin: 'https://evil.example.com' },
|
||||
popup,
|
||||
'https://api.example.com',
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts messages from the popup on the callback origin', () => {
|
||||
expect(
|
||||
isTrustedConnectorMessage(
|
||||
{ source: popup, origin: 'https://api.example.com' },
|
||||
popup,
|
||||
'https://api.example.com',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('falls back to the popup identity check when no origin is known', () => {
|
||||
expect(
|
||||
isTrustedConnectorMessage(
|
||||
{ source: popup, origin: 'https://api.example.com' },
|
||||
popup,
|
||||
null,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Whether a window message is the OAuth result posted by the connector popup we opened.
|
||||
* `expectedOrigin` is the callback origin reported by the backend, when known.
|
||||
*/
|
||||
export const isTrustedConnectorMessage = (
|
||||
event: Pick<MessageEvent, 'origin' | 'source'>,
|
||||
authWindow: Window | null,
|
||||
expectedOrigin?: string | null,
|
||||
): boolean => {
|
||||
if (!authWindow || event.source !== authWindow) return false;
|
||||
return !expectedOrigin || event.origin === expectedOrigin;
|
||||
};
|
||||
Reference in new issue
Block a user