forked from directus/directus
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix URL Redirection in OAuth2/OpenID/SAML (directus#21238)
Co-authored-by: Pascal Jufer <[email protected]> Co-authored-by: Azri Kahar <[email protected]>
- Loading branch information
1 parent
068591d
commit 5477d7d
Showing
11 changed files
with
261 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@directus/api": patch | ||
"@directus/env": patch | ||
--- | ||
|
||
Fixed potential Open Redirect vulnerability with OAuth2/OpenID/SAML SSO providers (via Directus redirect query parameter), by requiring redirect URLs to be enabled via allow list |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { vi, expect, test, afterEach } from 'vitest'; | ||
import { useEnv } from '@directus/env'; | ||
import { isLoginRedirectAllowed } from './is-login-redirect-allowed.js'; | ||
|
||
vi.mock('@directus/env'); | ||
|
||
afterEach(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
|
||
test('isLoginRedirectAllowed returns true with no redirect', () => { | ||
const redirect = undefined; | ||
const provider = 'local'; | ||
|
||
expect(isLoginRedirectAllowed(redirect, provider)).toBe(true); | ||
}); | ||
|
||
test('isLoginRedirectAllowed returns false with invalid redirect', () => { | ||
const redirect = 123456; | ||
const provider = 'local'; | ||
|
||
expect(isLoginRedirectAllowed(redirect, provider)).toBe(false); | ||
}); | ||
|
||
test('isLoginRedirectAllowed returns true for allowed URL', () => { | ||
const provider = 'local'; | ||
|
||
vi.mocked(useEnv).mockReturnValue({ | ||
[`AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`]: | ||
'http://external.example.com,https://external.example.com,http://external.example.com:8055/test', | ||
PUBLIC_URL: 'http://public.example.com', | ||
}); | ||
|
||
expect(isLoginRedirectAllowed('http://public.example.com', provider)).toBe(true); | ||
expect(isLoginRedirectAllowed('http://external.example.com', provider)).toBe(true); | ||
expect(isLoginRedirectAllowed('https://external.example.com', provider)).toBe(true); | ||
expect(isLoginRedirectAllowed('http://external.example.com:8055/test', provider)).toBe(true); | ||
}); | ||
|
||
test('isLoginRedirectAllowed returns false for denied URL', () => { | ||
const provider = 'local'; | ||
|
||
vi.mocked(useEnv).mockReturnValue({ | ||
[`AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`]: 'http://external.example.com', | ||
PUBLIC_URL: 'http://public.example.com', | ||
}); | ||
|
||
expect(isLoginRedirectAllowed('https://external.example.com', provider)).toBe(false); | ||
expect(isLoginRedirectAllowed('http://external.example.com:8055', provider)).toBe(false); | ||
expect(isLoginRedirectAllowed('http://external.example.com/test', provider)).toBe(false); | ||
}); | ||
|
||
test('isLoginRedirectAllowed returns true for relative paths', () => { | ||
const provider = 'local'; | ||
|
||
vi.mocked(useEnv).mockReturnValue({ | ||
[`AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`]: 'http://external.example.com', | ||
PUBLIC_URL: 'http://public.example.com', | ||
}); | ||
|
||
expect(isLoginRedirectAllowed('/admin/content', provider)).toBe(true); | ||
expect(isLoginRedirectAllowed('../admin/content', provider)).toBe(true); | ||
expect(isLoginRedirectAllowed('./admin/content', provider)).toBe(true); | ||
|
||
expect(isLoginRedirectAllowed('http://public.example.com/admin/content', provider)).toBe(true); | ||
}); | ||
|
||
test('isLoginRedirectAllowed returns false if missing protocol', () => { | ||
const provider = 'local'; | ||
|
||
vi.mocked(useEnv).mockReturnValue({ | ||
[`AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`]: 'http://example.com', | ||
PUBLIC_URL: 'http://example.com', | ||
}); | ||
|
||
expect(isLoginRedirectAllowed('//example.com/admin/content', provider)).toBe(false); | ||
expect(isLoginRedirectAllowed('//user@password:example.com/', provider)).toBe(false); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { useEnv } from '@directus/env'; | ||
import { toArray } from '@directus/utils'; | ||
import isUrlAllowed from './is-url-allowed.js'; | ||
|
||
/** | ||
* Checks if the defined redirect after successful SSO login is in the allow list | ||
*/ | ||
export function isLoginRedirectAllowed(redirect: unknown, provider: string): boolean { | ||
if (!redirect) return true; // empty redirect | ||
if (typeof redirect !== 'string') return false; // invalid type | ||
|
||
const env = useEnv(); | ||
const publicUrl = env['PUBLIC_URL'] as string; | ||
|
||
if (URL.canParse(redirect) === false) { | ||
if (redirect.startsWith('//') === false) { | ||
// should be a relative path like `/admin/test` | ||
return true; | ||
} | ||
|
||
// domain without protocol `//example.com/test` | ||
return false; | ||
} | ||
|
||
const { protocol: redirectProtocol, hostname: redirectDomain } = new URL(redirect); | ||
|
||
const envKey = `AUTH_${provider.toUpperCase()}_REDIRECT_ALLOW_LIST`; | ||
|
||
if (envKey in env) { | ||
if (isUrlAllowed(redirect, [...toArray(env[envKey] as string), publicUrl])) return true; | ||
} | ||
|
||
if (URL.canParse(publicUrl) === false) { | ||
return false; | ||
} | ||
|
||
// allow redirects to the defined PUBLIC_URL | ||
const { protocol: publicProtocol, hostname: publicDomain } = new URL(publicUrl); | ||
|
||
return `${redirectProtocol}//${redirectDomain}` === `${publicProtocol}//${publicDomain}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { expect, test } from 'vitest'; | ||
import isUrlAllowed from './is-url-allowed.js'; | ||
|
||
test('isUrlAllowed should allow matching domain', () => { | ||
const checkUrl = 'https://directus.io'; | ||
const allowedUrls = ['https://directus.io/']; | ||
|
||
expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(true); | ||
}); | ||
|
||
test('isUrlAllowed should allow matching path', () => { | ||
const checkUrl = 'https://directus.io/tv'; | ||
const allowedUrls = ['https://directus.io/tv']; | ||
|
||
expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(true); | ||
}); | ||
|
||
test('isUrlAllowed should block different paths', () => { | ||
const checkUrl = 'http://example.com/test1'; | ||
const allowedUrls = ['http://example.com/test2', 'http://example.com/test3', 'http://example.com/']; | ||
|
||
expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(false); | ||
}); | ||
|
||
test('isUrlAllowed should block different domains', () => { | ||
const checkUrl = 'http://directus.io/'; | ||
const allowedUrls = ['http://example.com/', 'http://directus.chat']; | ||
|
||
expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(false); | ||
}); | ||
|
||
test('isUrlAllowed blocks varying protocols', () => { | ||
const checkUrl = 'http://example.com/'; | ||
const allowedUrls = ['ftp://example.com/', 'https://example.com/']; | ||
|
||
expect(isUrlAllowed(checkUrl, allowedUrls)).toBe(false); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.