diff --git a/packages/web/src/pages/oauth-login-page/OAuthLoginPage.module.css b/packages/web/src/pages/oauth-login-page/OAuthLoginPage.module.css index 999dff33709..eec1c5d72fc 100644 --- a/packages/web/src/pages/oauth-login-page/OAuthLoginPage.module.css +++ b/packages/web/src/pages/oauth-login-page/OAuthLoginPage.module.css @@ -4,10 +4,13 @@ left: 0; right: 0; bottom: 0; - justify-content: center; - align-items: center; display: flex; - overflow: scroll; + justify-content: center; + overflow: auto; +} + +.wrapper > * { + margin-block: auto; } .popup { diff --git a/packages/web/src/pages/oauth-login-page/utils.test.ts b/packages/web/src/pages/oauth-login-page/utils.test.ts new file mode 100644 index 00000000000..81ac405d9d8 --- /dev/null +++ b/packages/web/src/pages/oauth-login-page/utils.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest' + +import { getIsRedirectValid } from './utils' + +const url = (s: string) => new URL(s) + +describe('getIsRedirectValid', () => { + // ── null / missing redirect URI ──────────────────────────────────────────── + + it('returns false when redirectUri is null', () => { + expect( + getIsRedirectValid({ parsedRedirectUri: null, redirectUri: null }) + ).toBe(false) + }) + + it('returns false when parsedRedirectUri is null but redirectUri is set', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: null, + redirectUri: 'not-a-valid-url' + }) + ).toBe(false) + }) + + // ── postMessage ──────────────────────────────────────────────────────────── + + it('returns true for postmessage', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: 'postmessage', + redirectUri: 'postMessage' + }) + ).toBe(true) + }) + + // ── dangerous schemes blocked ────────────────────────────────────────────── + + it('returns false for javascript: scheme', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: url('javascript:alert(1)'), + redirectUri: 'javascript:alert(1)' + }) + ).toBe(false) + }) + + it('returns false for data: scheme', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: url('data:text/html,

hi

'), + redirectUri: 'data:text/html,

hi

' + }) + ).toBe(false) + }) + + it('returns false for vbscript: scheme', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: url('vbscript:MsgBox(1)'), + redirectUri: 'vbscript:MsgBox(1)' + }) + ).toBe(false) + }) + + // ── https / http allowed ─────────────────────────────────────────────────── + + it('returns true for https redirect URI', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: url('https://yourapp.com/callback'), + redirectUri: 'https://yourapp.com/callback' + }) + ).toBe(true) + }) + + it('returns true for http redirect URI', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: url('http://localhost:3000/callback'), + redirectUri: 'http://localhost:3000/callback' + }) + ).toBe(true) + }) + + // ── custom URI schemes allowed (required for native apps) ───────────────── + + it('returns true for custom URI scheme (mobile deep link)', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: url('myapp://oauth/callback'), + redirectUri: 'myapp://oauth/callback' + }) + ).toBe(true) + }) + + it('returns true for audiusupload:// scheme', () => { + expect( + getIsRedirectValid({ + parsedRedirectUri: url('audiusupload://oauth/callback'), + redirectUri: 'audiusupload://oauth/callback' + }) + ).toBe(true) + }) +}) diff --git a/packages/web/src/pages/oauth-login-page/utils.ts b/packages/web/src/pages/oauth-login-page/utils.ts index f7837180cb7..36d89e86e35 100644 --- a/packages/web/src/pages/oauth-login-page/utils.ts +++ b/packages/web/src/pages/oauth-login-page/utils.ts @@ -25,40 +25,14 @@ export const getIsRedirectValid = ({ if (parsedRedirectUri === 'postmessage') { return true } - const { hash, username, password, pathname, hostname, protocol } = - parsedRedirectUri - // Ensure that the redirect_uri protocol is http or https - // IMPORTANT: If this validation is not done, users can - // use the redirect_uri to execute arbitrary code on the host - // domain (e.g. audius.co). - if (protocol !== 'http:' && protocol !== 'https:') { + const { protocol } = parsedRedirectUri + // Only block schemes that could execute code directly in the browser. + // All other validation (allowed domains, path, etc.) is enforced server-side + // via the registered redirect URI list for the OAuth client. + const dangerousSchemes = ['javascript:', 'data:', 'vbscript:'] + if (dangerousSchemes.includes(protocol)) { return false } - if (hash || username || password) { - return false - } - if ( - pathname.includes('/..') || - pathname.includes('\\..') || - pathname.includes('../') - ) { - return false - } - - // From https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address: - const ipRegex = - /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/ - const localhostIPv4Regex = - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - // Disallow IP addresses as redirect URIs unless it's localhost - if ( - ipRegex.test(hostname) && - hostname !== '[::1]' && - !localhostIPv4Regex.test(hostname) - ) { - return false - } - // TODO(nkang): Potentially check URI against malware list like https://urlhaus-api.abuse.ch/#urlinfo return true } else { return false