diff --git a/ui/src/artist/ArtistExternalLink.jsx b/ui/src/artist/ArtistExternalLink.jsx
index 1b6d7456..a83972f1 100644
--- a/ui/src/artist/ArtistExternalLink.jsx
+++ b/ui/src/artist/ArtistExternalLink.jsx
@@ -4,7 +4,7 @@ import { IconButton, Tooltip, Link } from '@material-ui/core'
import { ImLastfm2 } from 'react-icons/im'
import MusicBrainz from '../icons/MusicBrainz'
-import { intersperse } from '../utils'
+import { intersperse, isLastFmURL } from '../utils'
import config from '../config'
import { makeStyles } from '@material-ui/core/styles'
@@ -38,13 +38,13 @@ const ArtistExternalLinks = ({ artistInfo, record }) => {
}
if (config.lastFMEnabled) {
- if (lastFMlink) {
+ if (lastFMlink && isLastFmURL(lastFMlink[2])) {
addLink(
lastFMlink[2],
'message.openIn.lastfm',
,
)
- } else if (artistInfo?.lastFmUrl) {
+ } else if (isLastFmURL(artistInfo?.lastFmUrl)) {
addLink(
artistInfo?.lastFmUrl,
'message.openIn.lastfm',
diff --git a/ui/src/common/SafeHTML.jsx b/ui/src/common/SafeHTML.jsx
index 643b538b..98088356 100644
--- a/ui/src/common/SafeHTML.jsx
+++ b/ui/src/common/SafeHTML.jsx
@@ -1,5 +1,5 @@
import DOMPurify from 'dompurify'
-import { Fragment, useMemo } from 'react'
+import { useMemo } from 'react'
export const SafeHTML = ({ children }) => {
const purified = useMemo(() => {
@@ -23,5 +23,5 @@ export const SafeHTML = ({ children }) => {
return purify.sanitize(children, { ADD_ATTR: ['referrer-policy'] })
}, [children])
- return
+ return
}
diff --git a/ui/src/utils/urls.js b/ui/src/utils/urls.js
index 5788096d..80207fe8 100644
--- a/ui/src/utils/urls.js
+++ b/ui/src/utils/urls.js
@@ -44,3 +44,16 @@ export const shareCoverUrl = (id, square) => {
}
export const docsUrl = (path) => `https://www.navidrome.org${path}`
+
+export const isLastFmURL = (url) => {
+ try {
+ const parsed = new URL(url)
+ return (
+ (parsed.protocol === 'http:' || parsed.protocol === 'https:') &&
+ (parsed.hostname === 'last.fm' || parsed.hostname.endsWith('.last.fm')) &&
+ parsed.pathname.startsWith('/music/')
+ )
+ } catch (e) {
+ return false
+ }
+}
diff --git a/ui/src/utils/urls.test.js b/ui/src/utils/urls.test.js
new file mode 100644
index 00000000..26bdd128
--- /dev/null
+++ b/ui/src/utils/urls.test.js
@@ -0,0 +1,25 @@
+import { isLastFmURL } from './urls'
+
+describe('isLastFmURL', () => {
+ it('returns true for valid Last.fm music URLs', () => {
+ expect(isLastFmURL('https://last.fm/music/The+Beatles')).toBe(true)
+ expect(isLastFmURL('http://last.fm/music/Radiohead')).toBe(true)
+ expect(isLastFmURL('https://www.last.fm/music/Daft+Punk')).toBe(true)
+ })
+
+ it('returns false for non-http(s) protocols (XSS prevention)', () => {
+ expect(isLastFmURL('javascript:alert(1)//last.fm/music/')).toBe(false)
+ expect(isLastFmURL('data:text/html,