fix: block unsafe editor links

This commit is contained in:
lucaronin
2026-04-26 02:16:20 +02:00
parent 36e8a62284
commit 39234dcf52
4 changed files with 135 additions and 32 deletions

29
src/utils/url.test.ts Normal file
View File

@@ -0,0 +1,29 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { normalizeExternalUrl, openExternalUrl } from './url'
describe('normalizeExternalUrl', () => {
it('keeps valid http URLs and normalizes bare domains', () => {
expect(normalizeExternalUrl('https://example.com/docs')).toBe('https://example.com/docs')
expect(normalizeExternalUrl('example.com/docs')).toBe('https://example.com/docs')
})
it('rejects malformed or unsupported URLs', () => {
expect(normalizeExternalUrl('https://exa mple.com')).toBeNull()
expect(normalizeExternalUrl('javascript:alert(1)')).toBeNull()
expect(normalizeExternalUrl('not a url')).toBeNull()
})
})
describe('openExternalUrl', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('does not ask the browser to open malformed URLs', async () => {
const open = vi.spyOn(window, 'open').mockImplementation(() => null)
await openExternalUrl('https://exa mple.com')
expect(open).not.toHaveBeenCalled()
})
})

View File

@@ -2,24 +2,50 @@ import { isTauri } from '../mock-tauri'
const URL_PATTERN = /^https?:\/\//i
const BARE_DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z]{2,})+([/?#]|$)/i
const UNSAFE_URL_WHITESPACE_PATTERN = /\s/
export function normalizeExternalUrl(value: string): string | null {
const trimmed = value.trim()
if (!trimmed || UNSAFE_URL_WHITESPACE_PATTERN.test(trimmed)) return null
const candidate = URL_PATTERN.test(trimmed)
? trimmed
: BARE_DOMAIN_PATTERN.test(trimmed)
? `https://${trimmed}`
: null
if (!candidate) return null
try {
const parsed = new URL(candidate)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null
return candidate
} catch {
return null
}
}
export function isUrlValue(value: string): boolean {
if (!value) return false
return URL_PATTERN.test(value) || BARE_DOMAIN_PATTERN.test(value)
return normalizeExternalUrl(value) !== null
}
export function normalizeUrl(url: string): string {
const normalized = normalizeExternalUrl(url)
if (normalized) return normalized
if (URL_PATTERN.test(url)) return url
return `https://${url}`
}
/** Open a URL in the system browser. Uses Tauri opener plugin in native mode, window.open in browser. */
export async function openExternalUrl(url: string): Promise<void> {
const normalized = normalizeExternalUrl(url)
if (!normalized) return
if (isTauri()) {
const { openUrl } = await import('@tauri-apps/plugin-opener')
await openUrl(url)
await openUrl(normalized)
} else {
window.open(url, '_blank')
window.open(normalized, '_blank')
}
}