Navidrome is vulnerable to XSS via comment from song metadata
Description
Navidrome is an open source web-based music collection server and streamer. Prior to version 0.60.0, a cross-site scripting vulnerability in the frontend allows a malicious attacker to inject code through the comment metadata of a song to exfiltrate user credentials. This issue has been patched in version 0.60.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Navidrome before 0.60.0 has an XSS vulnerability in the frontend that allows attackers to inject code via song comment metadata to exfiltrate user credentials.
Vulnerability
Overview
Navidrome, an open-source web-based music collection server and streamer, is affected by a cross-site scripting (XSS) vulnerability in its frontend prior to version 0.60.0 [1][3]. The root cause lies in the use of React's dangerouslySetInnerHTML escape hatch within the MultiLineTextField component, which renders user-supplied content without proper sanitization [3]. Specifically, the comment metadata of a song or album is split into lines and rendered directly as HTML, allowing an attacker to inject arbitrary JavaScript code [3].
Exploitation
To exploit this vulnerability, an attacker must craft a malicious song file (e.g., a FLAC file) with a comment field containing an XSS payload, such as an ` tag with an onerror event handler [3]. The song must then be added to the Navidrome server (e.g., via file upload or library scanning) [3]. When a user views the song or album info page, the injected payload executes in the context of the user's browser session [3]. No authentication is required for the attacker to add the song, but the victim must be a legitimate user viewing the metadata [3].
Impact
Successful exploitation allows the attacker to execute arbitrary JavaScript in the victim's browser [3]. Since the Navidrome API token is stored in the browser's local storage and no Content Security Policy (CSP) is enforced by default, the attacker can exfiltrate the token to an external server an external server}) [3]. With the token, the attacker can impersonate the victim and access their music library, playlists, and other user-specific data [3].
Mitigation
The vulnerability has been patched in Navidrome version 0.60.0 [1][2]. Users are strongly advised to upgrade to this version or later [1]. As a temporary workaround, users can configure a Content Security Policy (CSP) in their reverse proxy or web server to mitigate the risk of data exfiltration [3]. No other mitigations are documented [3].
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/navidrome/navidromeGo | < 0.60.0 | 0.60.0 |
Affected products
2- navidrome/navidromev5Range: < 0.60.0
Patches
1d7ec7355c903Merge commit from fork
11 files changed · +99 −69
ui/package.json+1 −0 modified@@ -27,6 +27,7 @@ "clsx": "^2.1.1", "connected-react-router": "^6.9.3", "deepmerge": "^4.3.1", + "dompurify": "^3.3.1", "history": "^4.10.1", "inflection": "^3.0.2", "jwt-decode": "^4.0.0",
ui/package-lock.json+53 −4 modified@@ -18,6 +18,7 @@ "clsx": "^2.1.1", "connected-react-router": "^6.9.3", "deepmerge": "^4.3.1", + "dompurify": "^3.3.1", "history": "^4.10.1", "inflection": "^3.0.2", "jwt-decode": "^4.0.0", @@ -128,6 +129,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1742,6 +1744,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1765,6 +1768,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2572,6 +2576,7 @@ "resolved": "https://registry.npmjs.org/@jsonforms/core/-/core-2.5.2.tgz", "integrity": "sha512-tl64cLC2dUrGvu2nTHRDEA5Yv3RfwzMCIlVaoSUSq44LakKLGJdkPl8j/fb07llpFqz0a7gEAmy/8gLdmwgaLQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.3", "ajv": "^6.10.2", @@ -2625,6 +2630,7 @@ "resolved": "https://registry.npmjs.org/@jsonforms/react/-/react-2.5.2.tgz", "integrity": "sha512-kZf2fq4urIBlFTCiBX95eKg8uojkyJj7FVDtIV739aVkJjE5+ihn1+kG1qLxYSxlGC7S24i12BZJzRetSRihBQ==", "license": "MIT", + "peer": true, "dependencies": { "lodash": "^4.17.15", "object-hash": "^2.0.0" @@ -2640,6 +2646,7 @@ "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4", "@material-ui/styles": "^4.11.5", @@ -2686,6 +2693,7 @@ "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.4.4" }, @@ -3409,6 +3417,7 @@ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", "license": "MIT", + "peer": true, "dependencies": { "hoist-non-react-statics": "^3.3.0" }, @@ -3461,6 +3470,7 @@ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3482,6 +3492,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.90.tgz", "integrity": "sha512-P9beVR/x06U9rCJzSxtENnOr4BrbJ6VrsrDTc+73TtHv9XHhryXKbjGRB+6oooB2r0G/pQkD/S4dHo/7jUfwFw==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "^0.16", @@ -3651,6 +3662,7 @@ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -3982,6 +3994,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4663,6 +4676,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5038,6 +5052,7 @@ "resolved": "https://registry.npmjs.org/connected-react-router/-/connected-react-router-6.9.3.tgz", "integrity": "sha512-4ThxysOiv/R2Dc4Cke1eJwjKwH1Y51VDwlOrOfs1LjpdYOVvCNjNkZDayo7+sx42EeGJPQUNchWkjAIJdXGIOQ==", "license": "MIT", + "peer": true, "dependencies": { "lodash.isequalwith": "^4.4.0", "prop-types": "^15.7.2" @@ -5502,10 +5517,13 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "2.5.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", - "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", - "license": "(MPL-2.0 OR Apache-2.0)" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/dot-prop": { "version": "9.0.0", @@ -5919,6 +5937,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6502,6 +6521,7 @@ "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz", "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.10.0" }, @@ -6518,6 +6538,7 @@ "resolved": "https://registry.npmjs.org/final-form-arrays/-/final-form-arrays-3.1.0.tgz", "integrity": "sha512-TWBvun+AopgBLw9zfTFHBllnKMVNEwCEyDawphPuBGGqNsuhGzhT7yewHys64KFFwzIs6KEteGLpKOwvTQEscQ==", "license": "MIT", + "peer": true, "peerDependencies": { "final-form": "^4.20.8" } @@ -6932,6 +6953,7 @@ "integrity": "sha512-hM9gltmtQLfmWPqoPreUtRdP3nZCSzQEw7l/JC+up5CxquDykhYFKzIzoFFeVev3AGFEULNvsbE8fpZPgxUYEQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -7045,6 +7067,7 @@ "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -8653,6 +8676,7 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "license": "MIT", + "peer": true, "engines": { "node": "*" } @@ -9372,6 +9396,7 @@ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -9463,6 +9488,7 @@ "resolved": "https://registry.npmjs.org/ra-core/-/ra-core-3.19.12.tgz", "integrity": "sha512-E0cM6OjEUtccaR+dR5mL1MLiVVYML0Yf7aPhpLEq4iue73X3+CKcLztInoBhWgeevPbFQwgAtsXhlpedeyrNNg==", "license": "MIT", + "peer": true, "dependencies": { "classnames": "~2.3.1", "date-fns": "^1.29.0", @@ -9677,6 +9703,12 @@ "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", "license": "MIT" }, + "node_modules/ra-ui-materialui/node_modules/dompurify": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", + "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, "node_modules/ra-ui-materialui/node_modules/inflection": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", @@ -9852,6 +9884,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -9933,6 +9966,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -10005,6 +10039,7 @@ "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz", "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4" }, @@ -10022,6 +10057,7 @@ "resolved": "https://registry.npmjs.org/react-final-form-arrays/-/react-final-form-arrays-3.1.4.tgz", "integrity": "sha512-siVFAolUAe29rMR6u8VwepoysUcUdh6MLV2OWnCtKpsPRUdT9VUgECjAPaVMAH2GROZNiVB9On1H9MMrm9gdpg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.19.4" }, @@ -10127,6 +10163,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -10162,6 +10199,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -10182,6 +10220,7 @@ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -10363,6 +10402,7 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -10372,6 +10412,7 @@ "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.4.2.tgz", "integrity": "sha512-QLIn/q+7MX/B+MkGJ/K6R3//60eJ4QNy65eqPsJrfGezbxdh1Jx+37VRKE2K4PsJnNET5JufJtgWdT30WBa+6w==", "license": "MIT", + "peer": true, "dependencies": { "@redux-saga/core": "^1.4.2" } @@ -10625,6 +10666,7 @@ "integrity": "sha512-oWKZLjYwTihnTeINcNenxIIDfeotkQ2GAjFJPe7aYsMONrwDwQQXcAl3Qv0qON7Hdc8RTsFomq22zotm/i6VVQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -11585,6 +11627,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11806,6 +11849,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12050,6 +12094,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12174,6 +12219,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12187,6 +12233,7 @@ "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.17", "@vitest/mocker": "4.0.17", @@ -12703,6 +12750,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -12790,6 +12838,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" },
ui/src/album/AlbumDetails.jsx+4 −4 modified@@ -33,6 +33,7 @@ import { import config from '../config' import { formatFullDate, intersperse } from '../utils' import AlbumExternalLinks from './AlbumExternalLinks' +import { SafeHTML } from '../common/SafeHTML' const useStyles = makeStyles( (theme) => ({ @@ -225,8 +226,7 @@ const AlbumDetails = (props) => { const [imageLoading, setImageLoading] = useState(false) const [imageError, setImageError] = useState(false) - let notes = - albumInfo?.notes?.replace(new RegExp('<.*>', 'g'), '') || record.notes + let notes = albumInfo?.notes || record.notes if (notes) { notes += '..' @@ -351,7 +351,7 @@ const AlbumDetails = (props) => { variant={'body1'} onClick={() => setExpanded(!expanded)} > - <span dangerouslySetInnerHTML={{ __html: notes }} /> + <span><SafeHTML>{notes}</SafeHTML></span> </Typography> </Collapse> )} @@ -371,7 +371,7 @@ const AlbumDetails = (props) => { variant={'body1'} onClick={() => setExpanded(!expanded)} > - <span dangerouslySetInnerHTML={{ __html: notes }} /> + <span><SafeHTML>{notes}</SafeHTML></span> </Typography> </Collapse> </div>
ui/src/artist/ArtistShow.jsx+4 −14 modified@@ -1,4 +1,4 @@ -import React, { useState, createElement, useEffect } from 'react' +import { useState, useEffect } from 'react' import { useMediaQuery, withWidth } from '@material-ui/core' import { useShowController, @@ -53,9 +53,7 @@ const ArtistDetails = (props) => { const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('sm')) const [artistInfo, setArtistInfo] = useState() - const biography = - artistInfo?.biography?.replace(new RegExp('<.*>', 'g'), '') || - record.biography + const biography = artistInfo?.biography || record.biography useEffect(() => { subsonic @@ -72,16 +70,8 @@ const ArtistDetails = (props) => { }) }, [record.id]) - const component = isDesktop ? DesktopArtistDetails : MobileArtistDetails - return ( - <> - {createElement(component, { - artistInfo, - record, - biography, - })} - </> - ) + const Component = isDesktop ? DesktopArtistDetails : MobileArtistDetails + return <Component artistInfo={artistInfo} record={record} biography={biography} /> } const ArtistShowLayout = (props) => {
ui/src/artist/DesktopArtistDetails.jsx+2 −1 modified@@ -11,6 +11,7 @@ import Lightbox from 'react-image-lightbox' import ExpandInfoDialog from '../dialogs/ExpandInfoDialog' import AlbumInfo from '../album/AlbumInfo' import subsonic from '../subsonic' +import { SafeHTML } from '../common/SafeHTML' const useStyles = makeStyles( (theme) => ({ @@ -172,7 +173,7 @@ const DesktopArtistDetails = ({ artistInfo, record, biography }) => { variant={'body1'} onClick={() => setExpanded(!expanded)} > - <span dangerouslySetInnerHTML={{ __html: biography }} /> + <span><SafeHTML>{biography}</SafeHTML></span> </Typography> </Collapse> </CardContent>
ui/src/artist/MobileArtistDetails.jsx+2 −1 modified@@ -7,6 +7,7 @@ import config from '../config' import { LoveButton, RatingField } from '../common' import Lightbox from 'react-image-lightbox' import subsonic from '../subsonic' +import { SafeHTML } from '../common/SafeHTML' const useStyles = makeStyles( (theme) => ({ @@ -168,7 +169,7 @@ const MobileArtistDetails = ({ artistInfo, biography, record }) => { <div className={classes.biography}> <Collapse collapsedHeight={'1.5em'} in={expanded} timeout={'auto'}> <Typography variant={'body1'} onClick={() => setExpanded(!expanded)}> - <span dangerouslySetInnerHTML={{ __html: biography }} /> + <span><SafeHTML>{biography}</SafeHTML></span> </Typography> </Collapse> </div>
ui/src/common/Linkify.jsx+1 −6 modified@@ -53,12 +53,7 @@ const Linkify = ({ text, ...rest }) => { // Push remaining text if (text.length > lastIndex) { - elements.push( - <span - key={'last-span-key'} - dangerouslySetInnerHTML={{ __html: text.substring(lastIndex) }} - />, - ) + elements.push(text.substring(lastIndex)) } return elements.length === 1 ? elements[0] : elements
ui/src/common/MultiLineTextField.jsx+1 −11 modified@@ -30,17 +30,7 @@ export const MultiLineTextField = memo( > {lines.length === 0 && emptyText ? emptyText - : lines.map((line, idx) => - line === '' ? ( - <br key={md5(line + idx)} /> - ) : ( - <div - data-testid={`${source}.${idx}`} - key={md5(line + idx)} - dangerouslySetInnerHTML={{ __html: line }} - /> - ), - )} + : lines} </Typography> ) },
ui/src/common/MultiLineTextField.test.jsx+0 −28 removed@@ -1,28 +0,0 @@ -import * as React from 'react' -import { render, cleanup, screen } from '@testing-library/react' -import { MultiLineTextField } from './MultiLineTextField' - -describe('<MultiLineTextField />', () => { - afterEach(cleanup) - - it('should render each line in a separated div', () => { - const record = { comment: 'line1\nline2' } - render(<MultiLineTextField record={record} source={'comment'} />) - expect(screen.queryByTestId('comment.0').textContent).toBe('line1') - expect(screen.queryByTestId('comment.1').textContent).toBe('line2') - }) - - it.each([null, undefined])( - 'should render the emptyText when value is %s', - (body) => { - render( - <MultiLineTextField - record={{ id: 123, body }} - emptyText="NA" - source="body" - />, - ) - expect(screen.getByText('NA')).toBeInTheDocument() - }, - ) -})
ui/src/common/SafeHTML.jsx+29 −0 added@@ -0,0 +1,29 @@ +import DOMPurify from 'dompurify' +import { Fragment, useMemo } from 'react' + +export const SafeHTML = ({ + children, +}) => { + const purified = useMemo(() => { + const purify = DOMPurify() + + purify.addHook('afterSanitizeElements', async (node) => { + if (node instanceof HTMLElement) { + // Set referrer-policy for elements with src + switch (node.tagName.toLowerCase()) { + case 'a': + case 'area': + case 'img': + case 'video': + case 'iframe': + case 'script': + node.setAttribute('referrer-policy', 'no-referrer') + } + } + }) + + return purify.sanitize(children, { ADD_ATTR: ['referrer-policy'] }) + }, [children]) + + return <Fragment dangerouslySetInnerHTML={{ __html: purified }} /> +}
ui/src/layout/Login.jsx+2 −0 modified@@ -136,6 +136,8 @@ const FormLogin = ({ loading, handleSubmit, validate }) => { {config.welcomeMessage && ( <div className={classes.welcome} + // Use dangerouslySetInnerHTML to allow admins to configure + // whatever content they want dangerouslySetInnerHTML={{ __html: config.welcomeMessage }} /> )}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-rh3r-8pxm-hg4wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25578ghsaADVISORY
- github.com/navidrome/navidrome/commit/d7ec7355c9036d5be659d6ac555c334bb5848ba6ghsax_refsource_MISCWEB
- github.com/navidrome/navidrome/releases/tag/v0.60.0ghsax_refsource_MISCWEB
- github.com/navidrome/navidrome/security/advisories/GHSA-rh3r-8pxm-hg4wghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.