VYPR
Medium severity5.4NVD Advisory· Published Apr 15, 2026· Updated Apr 20, 2026

CVE-2026-33889

CVE-2026-33889

Description

ApostropheCMS is an open-source Node.js content management system. Versions 4.28.0 and prior contain a stored cross-site scripting vulnerability in the @apostrophecms/color-field module, where color values prefixed with -- bypass TinyColor validation intended for CSS custom properties, and the launder.string() call performs only type coercion without stripping HTML metacharacters. These unsanitized values are then concatenated directly into <style> tags both in per-widget style elements rendered for all visitors and in the global stylesheet rendered for editors, with the output marked as safe HTML. An editor can inject a value which closes the style tag and executes arbitrary JavaScript in the browser of every visitor to any page containing the affected widget. This enables mass session hijacking, cookie theft, and privilege escalation to administrative control if an admin views draft content. This issue has been fixed in version 4.29.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
apostrophenpm
< 4.29.04.29.0

Affected products

1

Patches

1
6a89bdb7acdb

Merge commit from fork

https://github.com/apostrophecms/apostropheTom BoutellApr 15, 2026via ghsa
4 files changed · +57 5
  • .changeset/orange-toes-battle.md+6 0 added
    @@ -0,0 +1,6 @@
    +---
    +"apostrophe": patch
    +---
    +
    +Security: fixed an XSS vulnerability. Color fields formerly accepted -- followed by anything, including </style>, which could be used to inject other markup. Thanks to [restriction](https://github.com/restriction) for reporting the issue and proposing the fix.
    +
    
  • packages/apostrophe/modules/@apostrophecms/color-field/index.js+7 1 modified
    @@ -32,9 +32,15 @@ module.exports = {
                   throw self.apos.error('required');
                 }
     
    +            const isVariable = destination[field.name].startsWith('--');
                 const test = new TinyColor(destination[field.name]);
    -            if (!test.isValid && !destination[field.name].startsWith('--')) {
    +            if (!test.isValid && !isVariable) {
                   destination[field.name] = null;
    +            } else if (isVariable) {
    +              // CSS custom property names: only allow alphanumeric, hyphens, underscores
    +              if (!/^--[a-zA-Z0-9_-]+$/.test(destination[field.name])) {
    +                destination[field.name] = null;
    +              }
                 }
               },
               isEmpty: function (field, value) {
    
  • packages/apostrophe/modules/@apostrophecms/styles/lib/methods.js+5 1 modified
    @@ -125,14 +125,16 @@ module.exports = (self, options) => {
             });
           }
           if (!hasLink) {
    +        // Prevent an XSS attack even if a styles exploit is found
    +        const css = (req.data.global.stylesStylesheet || '').replaceAll('</', '<\\/');
             nodes.push({
               name: 'style',
               attrs: {
                 id: 'apos-styles-stylesheet'
               },
               body: [
                 {
    -              raw: req.data.global.stylesStylesheet || ''
    +              raw: css
                 }
               ]
             });
    @@ -231,6 +233,8 @@ module.exports = (self, options) => {
               transform: assetOptions.transform || null
             });
           }
    +      // Prevent an XSS attack even if a styles exploit is found
    +      css = css.replaceAll('</', '<\\/');
           return `<style data-apos-widget-style-for="${widgetId}" data-apos-widget-style-id="${styleId}">\n` +
             css +
             '\n</style>';
    
  • packages/apostrophe/test/schemas.js+39 3 modified
    @@ -38,6 +38,11 @@ describe('Schemas', function() {
           name: 'slug',
           label: 'Slug',
           type: 'slug'
    +    },
    +    {
    +      name: 'color',
    +      label: 'Color',
    +      type: 'color'
         }
       ];
     
    @@ -1371,26 +1376,57 @@ describe('Schemas', function() {
         const schema = apos.schema.compose({
           addFields: simpleFields
         });
    -    assert(schema.length === 4);
    +    assert(schema.length === 5);
         const input = {
           name: 'Bob Smith',
           address: '5017 Awesome Street\nPhiladelphia, PA 19147',
           irrelevant: 'Irrelevant',
    -      slug: 'This Is Cool'
    +      slug: 'This Is Cool',
    +      color: '#ddeeff'
         };
         const req = apos.task.getReq();
         const result = {};
         await apos.schema.convert(req, schema, input, result);
         // no irrelevant or missing fields
    -    assert(_.keys(result).length === 4);
    +    assert(_.keys(result).length === 5);
         // expected fields came through
         assert(result.name === input.name);
         assert(result.address === input.address);
    +    assert.strictEqual(result.color, '#ddeeff');
         // default
         assert(result.variety === undefined);
         assert(result.slug === 'this-is-cool');
       });
     
    +  it('should allow well-behaved CSS variable names', async function() {
    +    const schema = apos.schema.compose({
    +      addFields: simpleFields
    +    });
    +    const input = {
    +      color: '--somevar'
    +    };
    +    const req = apos.task.getReq();
    +    const result = {};
    +    await apos.schema.convert(req, schema, input, result);
    +    // expected fields came through
    +    assert.strictEqual(result.color, '--somevar');
    +  });
    +
    +  it('should NOT allow malicious CSS variable names', async function() {
    +    const schema = apos.schema.compose({
    +      addFields: simpleFields
    +    });
    +    const malicious = '--</style><script>alert("mwahahah")';
    +    const input = {
    +      color: malicious
    +    };
    +    const req = apos.task.getReq();
    +    const result = {};
    +    await apos.schema.convert(req, schema, input, result);
    +    // Should be blocked
    +    assert.notStrictEqual(result.color, malicious);
    +  });
    +
       it('should update a password if provided', async function() {
         const schema = apos.schema.compose({
           addFields: [
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.