VYPR
Moderate severityNVD Advisory· Published Feb 4, 2026· Updated Feb 5, 2026

Navidrome is vulnerable to XSS via comment from song metadata

CVE-2026-25578

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.

PackageAffected versionsPatched versions
github.com/navidrome/navidromeGo
< 0.60.00.60.0

Affected products

2

Patches

1
d7ec7355c903

Merge commit from fork

https://github.com/navidrome/navidromeAlex GustafssonFeb 3, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.