GHSA-8gr3-5j6f-25gp

Suggest an improvement
Source
https://github.com/advisories/GHSA-8gr3-5j6f-25gp
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-8gr3-5j6f-25gp/GHSA-8gr3-5j6f-25gp.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-8gr3-5j6f-25gp
Aliases
Published
2026-09-22T20:34:35Z
Modified
2026-09-22T21:00:03Z
Severity
  • 5.1 (Medium) CVSS_V4 - 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 CVSS Calculator
Summary
Novu: Stored XSS in In-App Inbox via notification redirect.url javascript: scheme
Details

Summary

The @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.

An 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:<payload>", 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.

When 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).

Result: 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).

Affected

novuhq/novu self-hosted and cloud, API <= v3.15.0; @novu/js <= 3.15.0 and @novu/react (Inbox component). Confirmed live-exploitable on v3.15.0 (Docker community compose, default config, default roles). Condition: an in-app (Inbox) channel is in use, the standard product configuration. Recipient must use a Chromium-based browser (Chrome, Edge); the javascript: execution does not occur where the browser blocks javascript: in window.open.

Root cause

packages/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. packages/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. apps/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. libs/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). libs/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. libs/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.

Reproduction

novuhq/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.

  1. Create a v1 in-app workflow with a redirect CTA carrying a javascript: URL and target=_self:
POST /v1/workflows   Authorization: ApiKey <key>
{"name":"poc","notificationGroupId":"<ng>","active":true,
 "steps":[{"template":{"type":"in_app","content":"click me",
   "cta":{"type":"redirect","data":{
     "url":"javascript:window.top.__X=document.domain;void 0","target":"_self"}}}}]}
  1. Trigger it to a subscriber, then read the feed with a subscriber token minted from the public application identifier (no secret):
POST /v1/inbox/session {"applicationIdentifier":"<appId>","subscriberId":"<sub>"}
GET  /v1/inbox/notifications   Authorization: Bearer <subscriber-jwt>
-> redirect: {"url":"javascript:window.top.__X=document.domain;void 0","target":"_self"}
  1. The subscriber clicks the notification in the @novu/js / @novu/react Inbox.

Live-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://<dashboard>: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.

Impact

  • Stored XSS in the recipient's browser, cross-principal (content author to end-user subscriber), persistent, replicated to every recipient of the workflow.
  • On the self-hosted dashboard origin (no CSP), theft of localStorage['self-hosted-jwt'] yields takeover of another member or admin account.
  • In a customer application embedding the inbox, session and token theft and authenticated actions in that origin.
  • Triggered by the lowest privilege that can author a workflow, default config, one recipient click.

Credit

Jan Kahmen, turingpoint (jan@turingpoint.de)

Database specific
{
    "cwe_ids":  [
        "CWE-79"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-09-22T20:34:35Z",
    "nvd_published_at":  "2026-09-22T16:17:53Z",
    "severity":  "MODERATE"
}
References

Affected packages

npm / @novu/js

Package

Name
@novu/js
View open source insights on deps.dev
Purl
pkg:npm/%40novu/js

Affected ranges

Type
SEMVER
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
3.18.0

Database specific

last_known_affected_version_range
"<= 3.17.0"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-8gr3-5j6f-25gp/GHSA-8gr3-5j6f-25gp.json"