Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
104 changes: 104 additions & 0 deletions packages/web/src/pages/oauth-login-page/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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,<h1>hi</h1>'),
redirectUri: 'data:text/html,<h1>hi</h1>'
})
).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)
})
})
38 changes: 6 additions & 32 deletions packages/web/src/pages/oauth-login-page/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading