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