{"id":"CVE-2026-112951","aliases":["EUVD-2026-105154","GHSA-vmgj-9cwm-cf3h"],"status":"published","title":"ApostropheCMS: Color field validation bypass allows stored XSS via HTML appended after rgb()/hsl() prefix","description":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.34.0, the color field accepts trailing content after a valid rgb() or hsl() prefix because its TinyColor validation does not require the entire value to match a color. An authenticated user with edit rights can store arbitrary HTML after a valid color prefix. When the stored value is rendered, the payload executes, resulting in stored XSS. This issue is fixed in version 4.34.0.","severity":{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00304,"percentile":0.23329,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-79"],"published":"2026-10-20T15:33:09Z","modified":"2026-10-28T02:31:14Z","fixes":[{"ecosystem":"npm","package":"apostrophe","fixed_in":["4.34.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"apostrophe","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.34.0"}]}]},{"source":"osv","ecosystem":"npm","package":"apostrophe","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"4.34.0"}]}]}],"products":[{"vendor":"apostrophecms","product":"apostrophe"}],"references":[{"url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-vmgj-9cwm-cf3h","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/apostrophecms/apostrophe/commit/06e4b377f3c360f9e89c32dccd7e505ad5ddf498","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/apostrophecms/apostrophe/commit/40b989b1a9dd23d30029b8ce7668634265cfe5ea","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/apostrophecms/apostrophe/releases/tag/apostrophe@4.34.0","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112951","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-vmgj-9cwm-cf3h"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/112xxx/CVE-2026-112951.json","tags":["advisory"]},{"url":"https://github.com/apostrophecms/apostrophe","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-112951","title":"ApostropheCMS: Color field validation bypass allows stored XSS via HTML appended after rgb()/hsl() prefix","description":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.34.0, the color field accepts trailing content after a valid rgb() or hsl() prefix because its TinyColor validation does not require the entire value to match a color. An authenticated user with edit rights can store arbitrary HTML after a valid color prefix. When the stored value is rendered, the payload executes, resulting in stored XSS. This issue is fixed in version 4.34.0.","status":"published","published":"2026-10-20T15:33:09.945Z","modified":"2026-10-20T16:43:32.126Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T13:44:40.64332Z"}],"euvd":[{"id":"EUVD-2026-105154","description":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.34.0, the color field accepts trailing content after a valid rgb() or hsl() prefix because its TinyColor validation does not require the entire value to match a color. An authenticated user with edit rights can store arbitrary HTML after a valid color prefix. When the stored value is rendered, the payload executes, resulting in stored XSS. This issue is fixed in version 4.34.0.","status":"published","published":"2026-10-20T15:33:09Z","modified":"2026-10-20T16:43:32Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T16:59:43.600473Z"}],"ghsa":[{"id":"GHSA-vmgj-9cwm-cf3h","title":"ApostropheCMS: Color field validation bypass allows stored XSS via HTML appended after rgb()/hsl() prefix","description":"### Summary\nThe color field accepts any string that *begins* with a valid `rgb()` / `hsl()` color, because `TinyColor.isValid` does not anchor its match at the end of the string. An authenticated user with edit rights can store arbitrary HTML after a valid color prefix. When the stored value is rendered , the payload executes, resulting in stored XSS.\n\n### Details\nServer-side validation of the color field relies on TinyColor:\n\n```js\nconst { TinyColor } = require('@ctrl/tinycolor');\nif (!new TinyColor(value).isValid) throw self.apos.error('invalid');\n```\n\nTinyColor's `rgb`/`hsl` matchers are not anchored at the end of the string, while hex matchers are. Tested with @ctrl/tinycolor\n\n| Input | `isValid` |\n|---|---|\n| `rgb(1,2,3)\u003cimg src=x onerror=alert(1)\u003e` | `true` |\n| `rgb(1,2,3)\u003c/textarea\u003e\u003cscript\u003ealert(1)\u003c/script\u003e` | `true` |\n| `hsl(0,100%,50%);--x:1}\u003c/style\u003e\u003cscript\u003ealert(1)\u003c/script\u003e` | `true` |\n| `#ff0000\u003cimg src=x onerror=alert(1)\u003e` | `false` |\n\nThe value is saved to MongoDB unchanged (not normalized to hex8).\n\n### Relation to GHSA-97v6-998m-fp4g\nGHSA-97v6 addressed CSS custom property injection escaping a `\u003cstyle\u003e` context. The root cause (validation that accepts trailing content after a valid color) still exists in \u003c= 4.32.2, so [FILL IN: either \"the style-tag sink remains exploitable via `\u003c/style\u003e`\" OR \"a separate HTML sink at X is exploitable\"]. Fixing the validation (requiring the full string to be a color, or storing only TinyColor's normalized output) would close both.\n\n### Impact\nAny user with edit permission can plant persistent JavaScript that runs in the browser of visitors and administrators viewing the affected page, enabling session hijacking and actions as an admin.\n\n### Suggested fix\nReject values unless the entire string is a valid color, e.g. normalize on save:\n```js\nconst c = new TinyColor(value);\nif (!c.isValid) throw self.apos.error('invalid');\nvalue = c.toHex8String(); // store only normalized output\n```","status":"published","published":"2026-10-21T17:05:44Z","modified":"2026-10-21T17:05:44Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:41:34.690842Z"}],"nvd":[{"id":"CVE-2026-112951","description":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.34.0, the color field accepts trailing content after a valid rgb() or hsl() prefix because its TinyColor validation does not require the entire value to match a color. An authenticated user with edit rights can store arbitrary HTML after a valid color prefix. When the stored value is rendered, the payload executes, resulting in stored XSS. This issue is fixed in version 4.34.0.","status":"published","published":"2026-10-20T16:17:05.94Z","modified":"2026-10-20T19:53:10.247Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:13:01.161484Z"}],"osv":[{"id":"CVE-2026-112951","title":"ApostropheCMS: Color field validation bypass allows stored XSS via HTML appended after rgb()/hsl() prefix","description":"ApostropheCMS is an open-source Node.js content management system. Prior to 4.34.0, the color field accepts trailing content after a valid rgb() or hsl() prefix because its TinyColor validation does not require the entire value to match a color. An authenticated user with edit rights can store arbitrary HTML after a valid color prefix. When the stored value is rendered, the payload executes, resulting in stored XSS. This issue is fixed in version 4.34.0.","status":"published","published":"2026-10-20T15:33:09.945Z","modified":"2026-10-28T02:31:14.193597319Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:44:56.310988Z"},{"id":"GHSA-vmgj-9cwm-cf3h","title":"ApostropheCMS: Color field validation bypass allows stored XSS via HTML appended after rgb()/hsl() prefix","description":"### Summary\nThe color field accepts any string that *begins* with a valid `rgb()` / `hsl()` color, because `TinyColor.isValid` does not anchor its match at the end of the string. An authenticated user with edit rights can store arbitrary HTML after a valid color prefix. When the stored value is rendered , the payload executes, resulting in stored XSS.\n\n### Details\nServer-side validation of the color field relies on TinyColor:\n\n```js\nconst { TinyColor } = require('@ctrl/tinycolor');\nif (!new TinyColor(value).isValid) throw self.apos.error('invalid');\n```\n\nTinyColor's `rgb`/`hsl` matchers are not anchored at the end of the string, while hex matchers are. Tested with @ctrl/tinycolor\n\n| Input | `isValid` |\n|---|---|\n| `rgb(1,2,3)\u003cimg src=x onerror=alert(1)\u003e` | `true` |\n| `rgb(1,2,3)\u003c/textarea\u003e\u003cscript\u003ealert(1)\u003c/script\u003e` | `true` |\n| `hsl(0,100%,50%);--x:1}\u003c/style\u003e\u003cscript\u003ealert(1)\u003c/script\u003e` | `true` |\n| `#ff0000\u003cimg src=x onerror=alert(1)\u003e` | `false` |\n\nThe value is saved to MongoDB unchanged (not normalized to hex8).\n\n### Relation to GHSA-97v6-998m-fp4g\nGHSA-97v6 addressed CSS custom property injection escaping a `\u003cstyle\u003e` context. The root cause (validation that accepts trailing content after a valid color) still exists in \u003c= 4.32.2, so [FILL IN: either \"the style-tag sink remains exploitable via `\u003c/style\u003e`\" OR \"a separate HTML sink at X is exploitable\"]. Fixing the validation (requiring the full string to be a color, or storing only TinyColor's normalized output) would close both.\n\n### Impact\nAny user with edit permission can plant persistent JavaScript that runs in the browser of visitors and administrators viewing the affected page, enabling session hijacking and actions as an admin.\n\n### Suggested fix\nReject values unless the entire string is a valid color, e.g. normalize on save:\n```js\nconst c = new TinyColor(value);\nif (!c.isValid) throw self.apos.error('invalid');\nvalue = c.toHex8String(); // store only normalized output\n```","status":"published","published":"2026-10-21T17:05:44Z","modified":"2026-10-21T17:15:08.647725268Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:57.348035Z"}]}}
