{"id":"CVE-2026-63466","aliases":["EUVD-2026-64042","GHSA-w4mq-xh27-6xpx"],"status":"published","title":"Unleash: Global Mustache.escape override disables HTML escaping process-wide, enabling Slack/Teams link-injection via unrestricted username","description":"Unleash is an open-source feature management platform. Prior to 8.0.3, FeatureEventFormatterMd.format in src/lib/addons/feature-event-formatter-md.ts assigns Mustache.escape to an identity function before rendering action and path templates. Because Mustache.escape is process-wide, the assignment disables escaping for subsequent Mustache.render calls in email-service.ts, webhook.ts, datadog.ts, and new-relic.ts. An editor-level user can place Slack or Microsoft Teams link syntax in an unrestricted username, trigger a feature event, and inject an attacker-labeled link into a trusted outbound notification channel, while other Mustache sinks remain unescaped until restart. This issue is fixed in version 8.0.3.","severity":{"version":"3.1","score":4.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00319,"percentile":0.25223,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-116"],"published":"2026-08-21T18:21:46Z","modified":"2026-10-28T02:30:38Z","fixes":[{"ecosystem":"npm","package":"unleash-server","fixed_in":["8.0.3"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"unleash-server","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"8.0.3"}]}]},{"source":"osv","ecosystem":"npm","package":"unleash-server","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"8.0.3"}]}]}],"products":[{"vendor":"unleash","product":"unleash"}],"references":[{"url":"https://github.com/Unleash/unleash/security/advisories/GHSA-w4mq-xh27-6xpx","tags":["x_refsource_CONFIRM","advisory","web"]},{"url":"https://github.com/Unleash/unleash/commit/002012cfdbedd2e9b7db9dc83b9f549f761db22e","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/Unleash/unleash/releases/tag/v8.0.3","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/advisories/GHSA-w4mq-xh27-6xpx"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/63xxx/CVE-2026-63466.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-63466","tags":["advisory"]},{"url":"https://github.com/Unleash/unleash","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-63466","title":"Unleash: Global Mustache.escape override disables HTML escaping process-wide, enabling Slack/Teams link-injection via unrestricted username","description":"Unleash is an open-source feature management platform. Prior to 8.0.3, FeatureEventFormatterMd.format in src/lib/addons/feature-event-formatter-md.ts assigns Mustache.escape to an identity function before rendering action and path templates. Because Mustache.escape is process-wide, the assignment disables escaping for subsequent Mustache.render calls in email-service.ts, webhook.ts, datadog.ts, and new-relic.ts. An editor-level user can place Slack or Microsoft Teams link syntax in an unrestricted username, trigger a feature event, and inject an attacker-labeled link into a trusted outbound notification channel, while other Mustache sinks remain unescaped until restart. This issue is fixed in version 8.0.3.","status":"published","published":"2026-08-21T18:21:46.526Z","modified":"2026-08-21T21:43:35.526Z","cvss":[{"version":"3.1","score":4.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-116"],"updated_at":"2026-11-02T14:19:55.914391Z"}],"euvd":[{"id":"EUVD-2026-64042","description":"Unleash is an open-source feature management platform. Prior to 8.0.3, FeatureEventFormatterMd.format in src/lib/addons/feature-event-formatter-md.ts assigns Mustache.escape to an identity function before rendering action and path templates. Because Mustache.escape is process-wide, the assignment disables escaping for subsequent Mustache.render calls in email-service.ts, webhook.ts, datadog.ts, and new-relic.ts. An editor-level user can place Slack or Microsoft Teams link syntax in an unrestricted username, trigger a feature event, and inject an attacker-labeled link into a trusted outbound notification channel, while other Mustache sinks remain unescaped until restart. This issue is fixed in version 8.0.3.","status":"published","published":"2026-08-21T18:21:46Z","modified":"2026-08-21T21:43:35Z","cvss":[{"version":"3.1","score":4.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:01:36.844009Z"}],"ghsa":[{"id":"GHSA-w4mq-xh27-6xpx","title":"Unleash: Global Mustache.escape override disables HTML escaping process-wide, enabling Slack/Teams link-injection via unrestricted username","description":"## Vulnerability Details\n\n**File**: `src/lib/addons/feature-event-formatter-md.ts`\n**Line**: 355 (in v8.0.1; `format()` method)\n\n### Root Cause\n\n`FeatureEventFormatterMd.format()` does:\n\n```ts\nMustache.escape = (text) =\u003e text;\nconst text = Mustache.render(action, context);\n```\n\n`mustache` (pinned `^4.2.0`, confirmed installed `4.2.0`) keeps `escape` as a **module-level singleton** (`mustache.js`: `mustache.escape = escapeHtml;`), read by every `Mustache.render()` call in the process unless a per-call `config.escape` override is passed (`var escape = this.getConfigEscape(config) || mustache.escape;`). Node's module cache guarantees every `import Mustache from 'mustache'` in the process — `feature-event-formatter-md.ts`, `email-service.ts`, `webhook.ts`, `datadog.ts`, `new-relic.ts` — shares the same object instance.\n\nThis assignment therefore **permanently disables HTML escaping for every other `Mustache.render()` call in the same Node process** (including `email-service.ts` templates) from the moment any single notification addon (Webhook, Slack legacy, Microsoft Teams, Datadog, New Relic) first formats any event, for the remaining lifetime of the process.\n\n`feature-event-formatter-md-events.ts` (`EVENT_MAP`) confirms the blast radius: nearly every event's `action` template interpolates attacker-controlled values with single-mustache (intended-to-be-escaped) syntax, most importantly `{{user}}`, which is `event.createdBy` — the acting account's `username` (or `email` if set; `src/lib/util/extract-user.ts`: `extractUsernameFromUser`). Neither `username` nor `name` have any charset/length validation anywhere in the codebase (`create-user-schema.ts`, `create-invited-user-schema.ts`, `user-service.ts:289` only does `Joi.assert(name, Joi.string(), 'Name')` — a type check, nothing more).\n\nSlack's own API docs require `\u0026`, `\u003c`, `\u003e` to be replaced with `\u0026amp;`, `\u0026lt;`, `\u0026gt;` before sending user-generated text, specifically so Slack's mrkdwn parser does not interpret it as `\u003curl|label\u003e` link syntax. Mustache's default `escapeHtml` happens to produce exactly those entities, so this was (likely unintentionally) the application's only defense against link-injection in chat notifications — and it is unconditionally switched off by the same code path that depends on it.\n\n### Attack Scenario\n1. Admin has a Webhook, Slack (legacy), Microsoft Teams, Datadog, or New Relic integration configured (a very common production setup for flag-change notifications).\n2. Attacker has (or self-registers, if public signup is enabled — `POST /invite/:token/signup` is `permission: NONE`) any **Editor**-level account and sets `username` to e.g. `evil\u003chttps://attacker.example/urgent-rollback|Click here to view incident\u003e`.\n3. Attacker performs any ordinary write action (create/update/toggle a feature flag — routine, no special privilege beyond Editor on one project).\n4. The configured addon's `handleEvent()` calls `this.msgFormatter.format(event)`, which mutates the global escape function and immediately renders the `{{user}}`-containing template with escaping disabled.\n5. The resulting message — containing the attacker's raw `\u003curl|label\u003e` Slack link syntax — is POSTed to the team's Slack/Teams channel or webhook endpoint and rendered as a real, clickable, attacker-labeled hyperlink inside a trusted automated notification feed.\n\n### Vulnerable Code\n```ts\nMustache.escape = (text) =\u003e text;\n\nconst text = Mustache.render(action, context);\nconst url = path\n    ? `${this.unleashUrl}${Mustache.render(path, context)}`\n    : undefined;\n```\n\n### Impact\n- Stored markdown/link-injection (phishing-link injection) into any configured outbound notification channel (Slack legacy, MS Teams, Webhook default markdown, Datadog, New Relic), using an attacker-controlled username — no admin privilege required, only Editor on a single project, and potentially reachable through public self-signup.\n- Secondary: loss of HTML escaping for any other reachable Mustache single-mustache placeholder process-wide until restart (increases severity of any other currently-unreached or future Mustache sink, e.g. email templates).\n- Tertiary: a custom Webhook `bodyTemplate` that interpolates raw event/user fields directly into a JSON string literal (rather than the pre-escaped `eventJson` field the code already provides for this purpose) can have its JSON structure broken by an attacker-controlled `\"` character once the global escape function is neutered.\n\n### Recommended Fix\nNever mutate the shared `Mustache.escape` global. Pass a local escape function via Mustache's per-call render option instead (supported and typed in `@types/mustache@4.2.6`'s `RenderOptions.escape`):\n\n```ts\nconst renderConfig = { escape: (text: string) =\u003e text };\nconst text = Mustache.render(action, context, undefined, renderConfig);\nconst url = path\n    ? `${this.unleashUrl}${Mustache.render(path, context, undefined, renderConfig)}`\n    : undefined;\n```\n\n### Verification\nDynamically confirmed on v8.0.1 in a local Docker lab (official `unleashorg/unleash-server:8.0.1` image + Postgres 15):\n- Created a Webhook addon with the addon UI's own placeholder `bodyTemplate` (`{{event.createdBy}}` etc.), pointed at a local listener.\n- Created an Editor-role user with `username = evil2\u003chttps://attacker.example/urgent-rollback|Click here to view incident\u003e` (accepted with HTTP 201, no sanitization).\n- Logged in as that user and created a feature flag (ordinary Editor action).\n- Captured webhook payload: `\"createdBy\": \"evil2\u003chttps://attacker.example/urgent-rollback|Click here to view incident\u003e\"` — `\u003c`, `\u003e`, `|` completely unescaped, live Slack link-injection syntax.\n- Control test with the same pinned `mustache@4.2.0` package confirmed the default (pre-bug) output for the same string would have been `evil2\u0026lt;https:\u0026#x2F;\u0026#x2F;attacker.example\u0026#x2F;urgent-rollback|Click here to view incident\u0026gt;` — i.e. the single global assignment is solely responsible for the unescaped output observed live.","status":"published","published":"2026-08-21T19:14:52Z","modified":"2026-08-21T19:14:52Z","cvss":[{"version":"3.1","score":4.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N","level":"medium"}],"cwes":["CWE-116"],"updated_at":"2026-11-02T14:41:04.755504Z"}],"nvd":[{"id":"CVE-2026-63466","description":"Unleash is an open-source feature management platform. Prior to 8.0.3, FeatureEventFormatterMd.format in src/lib/addons/feature-event-formatter-md.ts assigns Mustache.escape to an identity function before rendering action and path templates. Because Mustache.escape is process-wide, the assignment disables escaping for subsequent Mustache.render calls in email-service.ts, webhook.ts, datadog.ts, and new-relic.ts. An editor-level user can place Slack or Microsoft Teams link syntax in an unrestricted username, trigger a feature event, and inject an attacker-labeled link into a trusted outbound notification channel, while other Mustache sinks remain unescaped until restart. This issue is fixed in version 8.0.3.","status":"published","published":"2026-08-21T19:17:32.117Z","modified":"2026-10-14T19:10:00.297Z","cvss":[{"version":"3.1","score":4.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-116"],"updated_at":"2026-11-02T14:12:40.665005Z"}],"osv":[{"id":"CVE-2026-63466","title":"Unleash: Global Mustache.escape override disables HTML escaping process-wide, enabling Slack/Teams link-injection via unrestricted username","description":"Unleash is an open-source feature management platform. Prior to 8.0.3, FeatureEventFormatterMd.format in src/lib/addons/feature-event-formatter-md.ts assigns Mustache.escape to an identity function before rendering action and path templates. Because Mustache.escape is process-wide, the assignment disables escaping for subsequent Mustache.render calls in email-service.ts, webhook.ts, datadog.ts, and new-relic.ts. An editor-level user can place Slack or Microsoft Teams link syntax in an unrestricted username, trigger a feature event, and inject an attacker-labeled link into a trusted outbound notification channel, while other Mustache sinks remain unescaped until restart. This issue is fixed in version 8.0.3.","status":"published","published":"2026-08-21T18:21:46.526Z","modified":"2026-10-28T02:30:38.793995074Z","cvss":[{"version":"3.1","score":4.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N","level":"medium"}],"cwes":["CWE-116"],"updated_at":"2026-11-02T15:45:13.306117Z"},{"id":"GHSA-w4mq-xh27-6xpx","title":"Unleash: Global Mustache.escape override disables HTML escaping process-wide, enabling Slack/Teams link-injection via unrestricted username","description":"## Vulnerability Details\n\n**File**: `src/lib/addons/feature-event-formatter-md.ts`\n**Line**: 355 (in v8.0.1; `format()` method)\n\n### Root Cause\n\n`FeatureEventFormatterMd.format()` does:\n\n```ts\nMustache.escape = (text) =\u003e text;\nconst text = Mustache.render(action, context);\n```\n\n`mustache` (pinned `^4.2.0`, confirmed installed `4.2.0`) keeps `escape` as a **module-level singleton** (`mustache.js`: `mustache.escape = escapeHtml;`), read by every `Mustache.render()` call in the process unless a per-call `config.escape` override is passed (`var escape = this.getConfigEscape(config) || mustache.escape;`). Node's module cache guarantees every `import Mustache from 'mustache'` in the process — `feature-event-formatter-md.ts`, `email-service.ts`, `webhook.ts`, `datadog.ts`, `new-relic.ts` — shares the same object instance.\n\nThis assignment therefore **permanently disables HTML escaping for every other `Mustache.render()` call in the same Node process** (including `email-service.ts` templates) from the moment any single notification addon (Webhook, Slack legacy, Microsoft Teams, Datadog, New Relic) first formats any event, for the remaining lifetime of the process.\n\n`feature-event-formatter-md-events.ts` (`EVENT_MAP`) confirms the blast radius: nearly every event's `action` template interpolates attacker-controlled values with single-mustache (intended-to-be-escaped) syntax, most importantly `{{user}}`, which is `event.createdBy` — the acting account's `username` (or `email` if set; `src/lib/util/extract-user.ts`: `extractUsernameFromUser`). Neither `username` nor `name` have any charset/length validation anywhere in the codebase (`create-user-schema.ts`, `create-invited-user-schema.ts`, `user-service.ts:289` only does `Joi.assert(name, Joi.string(), 'Name')` — a type check, nothing more).\n\nSlack's own API docs require `\u0026`, `\u003c`, `\u003e` to be replaced with `\u0026amp;`, `\u0026lt;`, `\u0026gt;` before sending user-generated text, specifically so Slack's mrkdwn parser does not interpret it as `\u003curl|label\u003e` link syntax. Mustache's default `escapeHtml` happens to produce exactly those entities, so this was (likely unintentionally) the application's only defense against link-injection in chat notifications — and it is unconditionally switched off by the same code path that depends on it.\n\n### Attack Scenario\n1. Admin has a Webhook, Slack (legacy), Microsoft Teams, Datadog, or New Relic integration configured (a very common production setup for flag-change notifications).\n2. Attacker has (or self-registers, if public signup is enabled — `POST /invite/:token/signup` is `permission: NONE`) any **Editor**-level account and sets `username` to e.g. `evil\u003chttps://attacker.example/urgent-rollback|Click here to view incident\u003e`.\n3. Attacker performs any ordinary write action (create/update/toggle a feature flag — routine, no special privilege beyond Editor on one project).\n4. The configured addon's `handleEvent()` calls `this.msgFormatter.format(event)`, which mutates the global escape function and immediately renders the `{{user}}`-containing template with escaping disabled.\n5. The resulting message — containing the attacker's raw `\u003curl|label\u003e` Slack link syntax — is POSTed to the team's Slack/Teams channel or webhook endpoint and rendered as a real, clickable, attacker-labeled hyperlink inside a trusted automated notification feed.\n\n### Vulnerable Code\n```ts\nMustache.escape = (text) =\u003e text;\n\nconst text = Mustache.render(action, context);\nconst url = path\n    ? `${this.unleashUrl}${Mustache.render(path, context)}`\n    : undefined;\n```\n\n### Impact\n- Stored markdown/link-injection (phishing-link injection) into any configured outbound notification channel (Slack legacy, MS Teams, Webhook default markdown, Datadog, New Relic), using an attacker-controlled username — no admin privilege required, only Editor on a single project, and potentially reachable through public self-signup.\n- Secondary: loss of HTML escaping for any other reachable Mustache single-mustache placeholder process-wide until restart (increases severity of any other currently-unreached or future Mustache sink, e.g. email templates).\n- Tertiary: a custom Webhook `bodyTemplate` that interpolates raw event/user fields directly into a JSON string literal (rather than the pre-escaped `eventJson` field the code already provides for this purpose) can have its JSON structure broken by an attacker-controlled `\"` character once the global escape function is neutered.\n\n### Recommended Fix\nNever mutate the shared `Mustache.escape` global. Pass a local escape function via Mustache's per-call render option instead (supported and typed in `@types/mustache@4.2.6`'s `RenderOptions.escape`):\n\n```ts\nconst renderConfig = { escape: (text: string) =\u003e text };\nconst text = Mustache.render(action, context, undefined, renderConfig);\nconst url = path\n    ? `${this.unleashUrl}${Mustache.render(path, context, undefined, renderConfig)}`\n    : undefined;\n```\n\n### Verification\nDynamically confirmed on v8.0.1 in a local Docker lab (official `unleashorg/unleash-server:8.0.1` image + Postgres 15):\n- Created a Webhook addon with the addon UI's own placeholder `bodyTemplate` (`{{event.createdBy}}` etc.), pointed at a local listener.\n- Created an Editor-role user with `username = evil2\u003chttps://attacker.example/urgent-rollback|Click here to view incident\u003e` (accepted with HTTP 201, no sanitization).\n- Logged in as that user and created a feature flag (ordinary Editor action).\n- Captured webhook payload: `\"createdBy\": \"evil2\u003chttps://attacker.example/urgent-rollback|Click here to view incident\u003e\"` — `\u003c`, `\u003e`, `|` completely unescaped, live Slack link-injection syntax.\n- Control test with the same pinned `mustache@4.2.0` package confirmed the default (pre-bug) output for the same string would have been `evil2\u0026lt;https:\u0026#x2F;\u0026#x2F;attacker.example\u0026#x2F;urgent-rollback|Click here to view incident\u0026gt;` — i.e. the single global assignment is solely responsible for the unescaped output observed live.","status":"published","published":"2026-08-21T19:14:52Z","modified":"2026-08-21T19:40:58.377206Z","cvss":[{"version":"3.1","score":4.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N","level":"medium"}],"cwes":["CWE-116"],"updated_at":"2026-11-02T15:46:58.04805Z"}]}}
