{"id":"CVE-2026-75510","aliases":["EUVD-2026-84535","GHSA-8gr3-5j6f-25gp"],"status":"published","title":"Novu: Stored XSS in In-App Inbox via notification redirect.url javascript: scheme","description":"Novu provides an API for sending notifications through multiple channels. Prior to 3.18.0, Novu's @novu/js In-App Inbox and the @novu/react Inbox component accept a notification call-to-action redirect.url from the v1 cta.data object and pass it through apps/api/src/app/inbox/utils/notification-mapper.ts and packages/js/src/ui/components/Notification/DefaultNotification.tsx to the navigate function in packages/js/src/ui/context/InboxContext.tsx without validating its URL scheme. An authenticated organization member or environment API-key holder can store a javascript: redirect with target _self in an in-app workflow. When a recipient using a Chromium-based browser clicks the notification, window.open executes the redirect in the current inbox-hosting origin, which can expose session material and permit authenticated actions in a customer application or the self-hosted Novu dashboard. This issue is fixed in version 3.18.0.","severity":{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00483,"percentile":0.42111,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-79"],"published":"2026-09-22T15:47:19Z","modified":"2026-10-28T02:30:24Z","fixes":[{"ecosystem":"npm","package":"@novu/js","fixed_in":["3.18.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"@novu/js","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"3.17.0"},{"fixed":"3.18.0"}]}]},{"source":"osv","ecosystem":"npm","package":"@novu/js","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.18.0"}]}]}],"products":[{"vendor":"novuhq","product":"novu"}],"references":[{"url":"https://github.com/novuhq/novu/security/advisories/GHSA-8gr3-5j6f-25gp","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/novuhq/novu/pull/11453","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/novuhq/novu/commit/f105f3d41a4405a75f803634d49f15a967524d8a","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/novuhq/novu/releases/tag/v3.18.0","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-75510","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-8gr3-5j6f-25gp"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/75xxx/CVE-2026-75510.json","tags":["advisory"]},{"url":"https://github.com/novuhq/novu","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-75510","title":"Novu: Stored XSS in In-App Inbox via notification redirect.url javascript: scheme","description":"Novu provides an API for sending notifications through multiple channels. Prior to 3.18.0, Novu's @novu/js In-App Inbox and the @novu/react Inbox component accept a notification call-to-action redirect.url from the v1 cta.data object and pass it through apps/api/src/app/inbox/utils/notification-mapper.ts and packages/js/src/ui/components/Notification/DefaultNotification.tsx to the navigate function in packages/js/src/ui/context/InboxContext.tsx without validating its URL scheme. An authenticated organization member or environment API-key holder can store a javascript: redirect with target _self in an in-app workflow. When a recipient using a Chromium-based browser clicks the notification, window.open executes the redirect in the current inbox-hosting origin, which can expose session material and permit authenticated actions in a customer application or the self-hosted Novu dashboard. This issue is fixed in version 3.18.0.","status":"published","published":"2026-09-22T15:47:19.922Z","modified":"2026-09-22T15:59:27.761Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","level":"medium","assessor":"cna"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:20:07.260654Z"}],"euvd":[{"id":"EUVD-2026-84535","description":"Novu provides an API for sending notifications through multiple channels. Prior to 3.18.0, Novu's @novu/js In-App Inbox and the @novu/react Inbox component accept a notification call-to-action redirect.url from the v1 cta.data object and pass it through apps/api/src/app/inbox/utils/notification-mapper.ts and packages/js/src/ui/components/Notification/DefaultNotification.tsx to the navigate function in packages/js/src/ui/context/InboxContext.tsx without validating its URL scheme. An authenticated organization member or environment API-key holder can store a javascript: redirect with target _self in an in-app workflow. When a recipient using a Chromium-based browser clicks the notification, window.open executes the redirect in the current inbox-hosting origin, which can expose session material and permit authenticated actions in a customer application or the self-hosted Novu dashboard. This issue is fixed in version 3.18.0.","status":"published","published":"2026-09-22T15:47:19Z","modified":"2026-09-22T15:59:27Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","level":"medium"}],"updated_at":"2026-11-02T17:28:02.601428Z"}],"ghsa":[{"id":"GHSA-8gr3-5j6f-25gp","title":"Novu: Stored XSS in In-App Inbox via notification redirect.url javascript: scheme","description":"## Summary\n\nThe `@novu/js` In-App Inbox renderer passes a notification's `redirect.url` to `window.open()` with no URL-scheme validation. The value originates from a notification's call-to-action and is delivered to the recipient verbatim.\n\nAn authenticated organization member (or any holder of the environment API key) creates a v1 in-app workflow whose step CTA stores `cta.data = { url: \"javascript:\u003cpayload\u003e\", target: \"_self\" }`. The v1 message-template `cta.data` field is a Mongoose `Mixed` type, so the arbitrary `target` key is accepted and persisted. The server-side inbox mapper copies `cta.data.url` and `cta.data.target` into the notification's `redirect` object with no scheme check. Novu's v2 control schema validates redirect URLs against `redirectUrlRegex` (which rejects `javascript:`), proving the intended invariant; the v1 path and the client renderer do not enforce it.\n\nWhen the recipient clicks the notification, the inbox calls `window.open(url, \"_self\", \"noopener noreferrer\")`. In Chromium browsers a `javascript:` URL opened with `target=\"_self\"` executes in the current document origin (the default `_blank` is browser-blocked, so the attacker sets `_self`).\n\nResult: a low-privilege content author runs arbitrary JavaScript in the browser of every recipient who clicks, in the origin that hosts the inbox (the customer application or the self-hosted Novu dashboard, neither of which sends a CSP).\n\n## Affected\n\nnovuhq/novu self-hosted and cloud, API \u003c= v3.15.0; `@novu/js` \u003c= 3.15.0 and `@novu/react` (Inbox component). Confirmed live-exploitable on v3.15.0 (Docker community compose, default config, default roles).\nCondition: an in-app (Inbox) channel is in use, the standard product configuration.\nRecipient must use a Chromium-based browser (Chrome, Edge); the `javascript:` execution does not occur where the browser blocks `javascript:` in `window.open`.\n\n## Root cause\n\npackages/js/src/ui/context/InboxContext.tsx:108: `window.open(url, target ?? DEFAULT_TARGET, DEFAULT_REFERRER)` is reached for any URL not starting with `/`, with no scheme allowlist, so `javascript:` is passed through.\npackages/js/src/ui/components/Notification/DefaultNotification.tsx:103: the notification click handler calls `navigate(redirect.url, redirect.target)`, feeding the stored values into the sink.\napps/api/src/app/inbox/utils/notification-mapper.ts:85-89: maps `cta.data.url` and `cta.data.target` into `redirect` with no validation of either field.\nlibs/dal/src/repositories/message-template/message-template.schema.ts:41: `data: Schema.Types.Mixed` accepts the arbitrary `target` key (not present in the typed interface).\nlibs/application-generic/src/usecases/compile-in-app-template/compile-in-app-template.usecase.ts:35-36: the v1 render path handlebars-compiles `cta.data.url` and performs no scheme check.\nlibs/application-generic/src/schemas/control/in-app-control.schema.ts:24-27: the v2 control schema enforces `url: z.string().regex(redirectUrlRegex)` which rejects `javascript:`, the guard absent from the v1 path and the renderer.\n\n## Reproduction\n\nnovuhq/novu v3.15.0 Docker community compose, default config. Attacker holds an environment API key (or a member session); victim opens the inbox in Chromium.\n\n1. Create a v1 in-app workflow with a redirect CTA carrying a `javascript:` URL and `target=_self`:\n```\nPOST /v1/workflows   Authorization: ApiKey \u003ckey\u003e\n{\"name\":\"poc\",\"notificationGroupId\":\"\u003cng\u003e\",\"active\":true,\n \"steps\":[{\"template\":{\"type\":\"in_app\",\"content\":\"click me\",\n   \"cta\":{\"type\":\"redirect\",\"data\":{\n     \"url\":\"javascript:window.top.__X=document.domain;void 0\",\"target\":\"_self\"}}}}]}\n```\n2. Trigger it to a subscriber, then read the feed with a subscriber token minted from the public application identifier (no secret):\n```\nPOST /v1/inbox/session {\"applicationIdentifier\":\"\u003cappId\u003e\",\"subscriberId\":\"\u003csub\u003e\"}\nGET  /v1/inbox/notifications   Authorization: Bearer \u003csubscriber-jwt\u003e\n-\u003e redirect: {\"url\":\"javascript:window.top.__X=document.domain;void 0\",\"target\":\"_self\"}\n```\n3. The subscriber clicks the notification in the `@novu/js` / `@novu/react` Inbox.\n\nLive-verified: the stored `javascript:` URL is returned verbatim by the inbox feed, and the exact shipped `navigate()` logic invoked from a real click runs `window.open(url,\"_self\",...)`, executing the payload in the `http://\u003cdashboard\u003e:4000` origin (no CSP); `window.top.__X` was set to the page origin. On the self-hosted dashboard the test inbox bell uses `subscriberId = user.externalId` (apps/dashboard/src/components/inbox-button.tsx:102), so a member targets another member's id and the executing payload reads `localStorage['self-hosted-jwt']` (apps/dashboard/src/utils/self-hosted/jwt-manager.tsx:4), the dashboard session token.\n\n## Impact\n\n- Stored XSS in the recipient's browser, cross-principal (content author to end-user subscriber), persistent, replicated to every recipient of the workflow.\n- On the self-hosted dashboard origin (no CSP), theft of `localStorage['self-hosted-jwt']` yields takeover of another member or admin account.\n- In a customer application embedding the inbox, session and token theft and authenticated actions in that origin.\n- Triggered by the lowest privilege that can author a workflow, default config, one recipient click.\n\n## Credit\n\nJan Kahmen, [turingpoint](https://turingpoint.de) (jan@turingpoint.de)","status":"published","published":"2026-09-22T20:34:35Z","modified":"2026-09-22T20:34:36Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:41:19.455935Z"}],"nvd":[{"id":"CVE-2026-75510","description":"Novu provides an API for sending notifications through multiple channels. Prior to 3.18.0, Novu's @novu/js In-App Inbox and the @novu/react Inbox component accept a notification call-to-action redirect.url from the v1 cta.data object and pass it through apps/api/src/app/inbox/utils/notification-mapper.ts and packages/js/src/ui/components/Notification/DefaultNotification.tsx to the navigate function in packages/js/src/ui/context/InboxContext.tsx without validating its URL scheme. An authenticated organization member or environment API-key holder can store a javascript: redirect with target _self in an in-app workflow. When a recipient using a Chromium-based browser clicks the notification, window.open executes the redirect in the current inbox-hosting origin, which can expose session material and permit authenticated actions in a customer application or the self-hosted Novu dashboard. This issue is fixed in version 3.18.0.","status":"published","published":"2026-09-22T16:17:53.95Z","modified":"2026-09-24T21:25:27.05Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"medium","assessor":"secondary"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:12:50.368368Z"}],"osv":[{"id":"CVE-2026-75510","title":"Novu: Stored XSS in In-App Inbox via notification redirect.url javascript: scheme","description":"Novu provides an API for sending notifications through multiple channels. Prior to 3.18.0, Novu's @novu/js In-App Inbox and the @novu/react Inbox component accept a notification call-to-action redirect.url from the v1 cta.data object and pass it through apps/api/src/app/inbox/utils/notification-mapper.ts and packages/js/src/ui/components/Notification/DefaultNotification.tsx to the navigate function in packages/js/src/ui/context/InboxContext.tsx without validating its URL scheme. An authenticated organization member or environment API-key holder can store a javascript: redirect with target _self in an in-app workflow. When a recipient using a Chromium-based browser clicks the notification, window.open executes the redirect in the current inbox-hosting origin, which can expose session material and permit authenticated actions in a customer application or the self-hosted Novu dashboard. This issue is fixed in version 3.18.0.","status":"published","published":"2026-09-22T15:47:19.922Z","modified":"2026-10-28T02:30:24.926415035Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:45:17.102988Z"},{"id":"GHSA-8gr3-5j6f-25gp","title":"Novu: Stored XSS in In-App Inbox via notification redirect.url javascript: scheme","description":"## Summary\n\nThe `@novu/js` In-App Inbox renderer passes a notification's `redirect.url` to `window.open()` with no URL-scheme validation. The value originates from a notification's call-to-action and is delivered to the recipient verbatim.\n\nAn authenticated organization member (or any holder of the environment API key) creates a v1 in-app workflow whose step CTA stores `cta.data = { url: \"javascript:\u003cpayload\u003e\", target: \"_self\" }`. The v1 message-template `cta.data` field is a Mongoose `Mixed` type, so the arbitrary `target` key is accepted and persisted. The server-side inbox mapper copies `cta.data.url` and `cta.data.target` into the notification's `redirect` object with no scheme check. Novu's v2 control schema validates redirect URLs against `redirectUrlRegex` (which rejects `javascript:`), proving the intended invariant; the v1 path and the client renderer do not enforce it.\n\nWhen the recipient clicks the notification, the inbox calls `window.open(url, \"_self\", \"noopener noreferrer\")`. In Chromium browsers a `javascript:` URL opened with `target=\"_self\"` executes in the current document origin (the default `_blank` is browser-blocked, so the attacker sets `_self`).\n\nResult: a low-privilege content author runs arbitrary JavaScript in the browser of every recipient who clicks, in the origin that hosts the inbox (the customer application or the self-hosted Novu dashboard, neither of which sends a CSP).\n\n## Affected\n\nnovuhq/novu self-hosted and cloud, API \u003c= v3.15.0; `@novu/js` \u003c= 3.15.0 and `@novu/react` (Inbox component). Confirmed live-exploitable on v3.15.0 (Docker community compose, default config, default roles).\nCondition: an in-app (Inbox) channel is in use, the standard product configuration.\nRecipient must use a Chromium-based browser (Chrome, Edge); the `javascript:` execution does not occur where the browser blocks `javascript:` in `window.open`.\n\n## Root cause\n\npackages/js/src/ui/context/InboxContext.tsx:108: `window.open(url, target ?? DEFAULT_TARGET, DEFAULT_REFERRER)` is reached for any URL not starting with `/`, with no scheme allowlist, so `javascript:` is passed through.\npackages/js/src/ui/components/Notification/DefaultNotification.tsx:103: the notification click handler calls `navigate(redirect.url, redirect.target)`, feeding the stored values into the sink.\napps/api/src/app/inbox/utils/notification-mapper.ts:85-89: maps `cta.data.url` and `cta.data.target` into `redirect` with no validation of either field.\nlibs/dal/src/repositories/message-template/message-template.schema.ts:41: `data: Schema.Types.Mixed` accepts the arbitrary `target` key (not present in the typed interface).\nlibs/application-generic/src/usecases/compile-in-app-template/compile-in-app-template.usecase.ts:35-36: the v1 render path handlebars-compiles `cta.data.url` and performs no scheme check.\nlibs/application-generic/src/schemas/control/in-app-control.schema.ts:24-27: the v2 control schema enforces `url: z.string().regex(redirectUrlRegex)` which rejects `javascript:`, the guard absent from the v1 path and the renderer.\n\n## Reproduction\n\nnovuhq/novu v3.15.0 Docker community compose, default config. Attacker holds an environment API key (or a member session); victim opens the inbox in Chromium.\n\n1. Create a v1 in-app workflow with a redirect CTA carrying a `javascript:` URL and `target=_self`:\n```\nPOST /v1/workflows   Authorization: ApiKey \u003ckey\u003e\n{\"name\":\"poc\",\"notificationGroupId\":\"\u003cng\u003e\",\"active\":true,\n \"steps\":[{\"template\":{\"type\":\"in_app\",\"content\":\"click me\",\n   \"cta\":{\"type\":\"redirect\",\"data\":{\n     \"url\":\"javascript:window.top.__X=document.domain;void 0\",\"target\":\"_self\"}}}}]}\n```\n2. Trigger it to a subscriber, then read the feed with a subscriber token minted from the public application identifier (no secret):\n```\nPOST /v1/inbox/session {\"applicationIdentifier\":\"\u003cappId\u003e\",\"subscriberId\":\"\u003csub\u003e\"}\nGET  /v1/inbox/notifications   Authorization: Bearer \u003csubscriber-jwt\u003e\n-\u003e redirect: {\"url\":\"javascript:window.top.__X=document.domain;void 0\",\"target\":\"_self\"}\n```\n3. The subscriber clicks the notification in the `@novu/js` / `@novu/react` Inbox.\n\nLive-verified: the stored `javascript:` URL is returned verbatim by the inbox feed, and the exact shipped `navigate()` logic invoked from a real click runs `window.open(url,\"_self\",...)`, executing the payload in the `http://\u003cdashboard\u003e:4000` origin (no CSP); `window.top.__X` was set to the page origin. On the self-hosted dashboard the test inbox bell uses `subscriberId = user.externalId` (apps/dashboard/src/components/inbox-button.tsx:102), so a member targets another member's id and the executing payload reads `localStorage['self-hosted-jwt']` (apps/dashboard/src/utils/self-hosted/jwt-manager.tsx:4), the dashboard session token.\n\n## Impact\n\n- Stored XSS in the recipient's browser, cross-principal (content author to end-user subscriber), persistent, replicated to every recipient of the workflow.\n- On the self-hosted dashboard origin (no CSP), theft of `localStorage['self-hosted-jwt']` yields takeover of another member or admin account.\n- In a customer application embedding the inbox, session and token theft and authenticated actions in that origin.\n- Triggered by the lowest privilege that can author a workflow, default config, one recipient click.\n\n## Credit\n\nJan Kahmen, [turingpoint](https://turingpoint.de) (jan@turingpoint.de)","status":"published","published":"2026-09-22T20:34:35Z","modified":"2026-09-22T21:00:03.651537464Z","cvss":[{"version":"4.0","score":5.1,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:42.437513Z"}]}}
