VYPR
Moderate severityNVD Advisory· Published Nov 9, 2023· Updated Sep 4, 2024

CVE-2023-45884

CVE-2023-45884

Description

NASA Open MCT through 3.1.0 is vulnerable to a CSRF via the flexibleLayout plugin, allowing attackers to view sensitive information.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

NASA Open MCT through 3.1.0 is vulnerable to a CSRF via the flexibleLayout plugin, allowing attackers to view sensitive information.

Vulnerability

Overview

CVE-2023-45884 describes a Cross-Site Request Forgery (CSRF) vulnerability in NASA Open MCT (Open Mission Control Technologies) through version 3.1.0. The vulnerability resides in the flexibleLayout plugin, which fails to implement proper CSRF protection mechanisms. The software also lacks Content Security Policy (CSP) flags [1].

Exploitation

An attacker can craft a malicious page that, when visited by an authenticated Open MCT user, triggers unauthorized actions via the flexibleLayout plugin. Since the plugin does not validate or require a unique token for state-changing requests, the attacker can force the victim's browser to execute requests that view or manipulate sensitive data. No authentication is bypassed; the victim must be logged in [1][4].

Impact

Successful exploitation allows an attacker to view sensitive information accessible to the victim user. The CSRF vulnerability can be combined with other flaws (e.g., stored XSS, CVE-2023-45885) to escalate impact, potentially leading to data exfiltration or further compromise within the Open MCT instance [1].

Mitigation

As of publication, the vendor has addressed related input sanitization issues in subsequent commits (e.g., commit 4e95e12559c9c5364269ff366a59768573baacb4 which introduces the eslint-plugin-no-unsanitized rule) [3]. Users are advised to update to a patched version or apply appropriate CSRF and CSP controls if running an affected release [1][2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openmctnpm
< 3.1.13.1.1

Affected products

2

Patches

1
ff8288d10ec7

cherry-pick(#7144): fix(#7143): add `eslint-plugin-no-unsanitized` and fix errors (#7148)

https://github.com/nasa/openmctJesse MazzellaOct 23, 2023via ghsa-ref
11 files changed · +115 65
  • .eslintrc.js+2 1 modified
    @@ -15,7 +15,8 @@ module.exports = {
         'plugin:compat/recommended',
         'plugin:vue/vue3-recommended',
         'plugin:you-dont-need-lodash-underscore/compatible',
    -    'plugin:prettier/recommended'
    +    'plugin:prettier/recommended',
    +    'plugin:no-unsanitized/DOM'
       ],
       parser: 'vue-eslint-parser',
       parserOptions: {
    
  • package.json+1 0 modified
    @@ -27,6 +27,7 @@
         "eslint": "8.48.0",
         "eslint-config-prettier": "9.0.0",
         "eslint-plugin-compat": "4.2.0",
    +    "eslint-plugin-no-unsanitized": "4.0.2",
         "eslint-plugin-playwright": "0.12.0",
         "eslint-plugin-prettier": "4.2.1",
         "eslint-plugin-simple-import-sort": "10.0.0",
    
  • src/plugins/flexibleLayout/components/Frame.vue+4 1 modified
    @@ -161,8 +161,11 @@ export default {
             let originalClassName = this.dragGhost.classList[0];
             this.dragGhost.className = '';
             this.dragGhost.classList.add(originalClassName, iconClass);
    +        this.dragGhost.textContent = '';
    +        const span = document.createElement('span');
    +        span.textContent = this.domainObject.name;
    +        this.dragGhost.appendChild(span);
     
    -        this.dragGhost.innerHTML = `<span>${this.domainObject.name}</span>`;
             event.dataTransfer.setDragImage(this.dragGhost, 0, 0);
           }
     
    
  • src/plugins/plot/chart/MctChart.vue+16 7 modified
    @@ -20,12 +20,10 @@
      at runtime from the About dialog for additional information.
     -->
     
    -<!-- eslint-disable vue/no-v-html -->
    -
     <template>
       <div class="gl-plot-chart-area">
    -    <span v-html="canvasTemplate"></span>
    -    <span v-html="canvasTemplate"></span>
    +    <canvas :style="canvasStyle"></canvas>
    +    <canvas :style="canvasStyle"></canvas>
         <div ref="limitArea" class="js-limit-area">
           <limit-label
             v-for="(limitLabel, index) in visibleLimitLabels"
    @@ -146,12 +144,20 @@ export default {
       },
       data() {
         return {
    -      canvasTemplate:
    -        '<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>',
           visibleLimitLabels: [],
           visibleLimitLines: []
         };
       },
    +  computed: {
    +    canvasStyle() {
    +      return {
    +        position: 'absolute',
    +        background: 'none',
    +        width: '100%',
    +        height: '100%'
    +      };
    +    }
    +  },
       watch: {
         highlights: {
           handler() {
    @@ -487,7 +493,10 @@ export default {
           // Have to throw away the old canvas elements and replace with new
           // canvas elements in order to get new drawing contexts.
           const div = document.createElement('div');
    -      div.innerHTML = this.canvasTemplate + this.canvasTemplate;
    +      div.innerHTML = `
    +      <canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>
    +      <canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>
    +      `;
           const mainCanvas = div.querySelectorAll('canvas')[1];
           const overlayCanvas = div.querySelectorAll('canvas')[0];
           this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);
    
  • src/plugins/summaryWidget/src/Condition.js+8 4 modified
    @@ -214,7 +214,7 @@ define([
               const options = this.generateSelectOptions();
     
               newInput = document.createElement('select');
    -          newInput.innerHTML = options;
    +          newInput.appendChild(options);
     
               emitChange = true;
             } else {
    @@ -244,12 +244,16 @@ define([
     
       Condition.prototype.generateSelectOptions = function () {
         let telemetryMetadata = this.conditionManager.getTelemetryMetadata(this.config.object);
    -    let options = '';
    +    let fragment = document.createDocumentFragment();
    +
         telemetryMetadata[this.config.key].enumerations.forEach((enumeration) => {
    -      options += '<option value="' + enumeration.value + '">' + enumeration.string + '</option>';
    +      const option = document.createElement('option');
    +      option.value = enumeration.value;
    +      option.textContent = enumeration.string;
    +      fragment.appendChild(option);
         });
     
    -    return options;
    +    return fragment;
       };
     
       return Condition;
    
  • src/plugins/summaryWidget/src/input/Palette.js+6 5 modified
    @@ -44,11 +44,12 @@ define([
         self.setNullOption(this.nullOption);
     
         self.items.forEach(function (item) {
    -      const itemElement = `<div class = "c-palette__item ${item}" data-item = "${item}"></div>`;
    -      const temp = document.createElement('div');
    -      temp.innerHTML = itemElement;
    -      self.itemElements[item] = temp.firstChild;
    -      self.domElement.querySelector('.c-palette__items').appendChild(temp.firstChild);
    +      const itemElement = document.createElement('div');
    +      itemElement.className = 'c-palette__item ' + item;
    +      itemElement.setAttribute('data-item', item);
    +
    +      self.itemElements[item] = itemElement;
    +      self.domElement.querySelector('.c-palette__items').appendChild(itemElement);
         });
     
         self.domElement.querySelector('.c-menu').style.display = 'none';
    
  • src/plugins/summaryWidget/src/Rule.js+6 5 modified
    @@ -167,7 +167,8 @@ define([
             const ruleHeader = self.domElement
               .querySelectorAll('.widget-rule-header')[0]
               .cloneNode(true);
    -        indicator.innerHTML = ruleHeader;
    +        indicator.textContent = '';
    +        indicator.appendChild(ruleHeader);
           });
           self.widgetDnD.setDragImage(
             self.domElement.querySelectorAll('.widget-rule-header')[0].cloneNode(true)
    @@ -239,8 +240,8 @@ define([
         this.listenTo(this.toggleConfigButton, 'click', toggleConfig);
         this.listenTo(this.trigger, 'change', onTriggerInput);
     
    -    this.title.innerHTML = self.config.name;
    -    this.description.innerHTML = self.config.description;
    +    this.title.innerText = self.config.name;
    +    this.description.innerText = self.config.description;
         this.trigger.value = self.config.trigger;
     
         this.listenTo(this.grippy, 'mousedown', onDragStart);
    @@ -456,7 +457,7 @@ define([
             const lastOfType = self.conditionArea.querySelector('li:last-of-type');
             lastOfType.parentNode.insertBefore($condition, lastOfType);
             if (loopCnt > 0) {
    -          $condition.querySelector('.t-condition-context').innerHTML = triggerContextStr + ' when';
    +          $condition.querySelector('.t-condition-context').innerText = triggerContextStr + ' when';
             }
     
             loopCnt++;
    @@ -528,7 +529,7 @@ define([
         }
     
         description = description === '' ? this.config.description : description;
    -    this.description.innerHTML = self.config.description;
    +    this.description.innerText = self.config.description;
         this.config.description = description;
       };
     
    
  • src/plugins/summaryWidget/src/SummaryWidget.js+2 1 modified
    @@ -247,9 +247,10 @@ define([
       SummaryWidget.prototype.updateWidget = function () {
         const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
         const activeRule = this.rulesById[this.activeId];
    +
         this.applyStyle(this.domElement.querySelector('#widget'), activeRule.getProperty('style'));
         this.domElement.querySelector('#widget').title = activeRule.getProperty('message');
    -    this.domElement.querySelector('#widgetLabel').innerHTML = activeRule.getProperty('label');
    +    this.domElement.querySelector('#widgetLabel').textContent = activeRule.getProperty('label');
         this.domElement.querySelector('#widgetIcon').classList =
           WIDGET_ICON_CLASS + ' ' + activeRule.getProperty('icon');
       };
    
  • src/plugins/summaryWidget/src/views/summary-widget.html+0 4 removed
    @@ -1,4 +0,0 @@
    -<a class="t-summary-widget c-summary-widget js-sw u-links u-fills-container">
    -  <div id="widgetIcon" class="c-sw__icon js-sw__icon"></div>
    -  <div id="widgetLabel" class="c-sw__label js-sw__label">Loading...</div>
    -</a>
    
  • src/plugins/summaryWidget/src/views/SummaryWidgetView.js+58 34 modified
    @@ -1,35 +1,60 @@
    -define(['./summary-widget.html', '@braintree/sanitize-url'], function (
    -  summaryWidgetTemplate,
    -  urlSanitizeLib
    -) {
    -  const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
    +import * as urlSanitizeLib from '@braintree/sanitize-url';
     
    -  function SummaryWidgetView(domainObject, openmct) {
    +const WIDGET_ICON_CLASS = 'c-sw__icon js-sw__icon';
    +
    +class SummaryWidgetView {
    +  #createSummaryWidgetTemplate() {
    +    const anchor = document.createElement('a');
    +    anchor.classList.add(
    +      't-summary-widget',
    +      'c-summary-widget',
    +      'js-sw',
    +      'u-links',
    +      'u-fills-container'
    +    );
    +
    +    const widgetIcon = document.createElement('div');
    +    widgetIcon.id = 'widgetIcon';
    +    widgetIcon.classList.add('c-sw__icon', 'js-sw__icon');
    +    anchor.appendChild(widgetIcon);
    +
    +    const widgetLabel = document.createElement('div');
    +    widgetLabel.id = 'widgetLabel';
    +    widgetLabel.classList.add('c-sw__label', 'js-sw__label');
    +    widgetLabel.textContent = 'Loading...';
    +    anchor.appendChild(widgetLabel);
    +
    +    return anchor;
    +  }
    +
    +  constructor(domainObject, openmct) {
         this.openmct = openmct;
         this.domainObject = domainObject;
         this.hasUpdated = false;
         this.render = this.render.bind(this);
       }
     
    -  SummaryWidgetView.prototype.updateState = function (datum) {
    +  updateState(datum) {
         this.hasUpdated = true;
         this.widget.style.color = datum.textColor;
         this.widget.style.backgroundColor = datum.backgroundColor;
         this.widget.style.borderColor = datum.borderColor;
         this.widget.title = datum.message;
         this.label.title = datum.message;
    -    this.label.innerHTML = datum.ruleLabel;
    +    this.label.textContent = datum.ruleLabel;
         this.icon.className = WIDGET_ICON_CLASS + ' ' + datum.icon;
    -  };
    +  }
     
    -  SummaryWidgetView.prototype.render = function () {
    +  render() {
         if (this.unsubscribe) {
           this.unsubscribe();
         }
     
         this.hasUpdated = false;
     
    -    this.container.innerHTML = summaryWidgetTemplate;
    +    const anchor = this.#createSummaryWidgetTemplate();
    +    this.container.appendChild(anchor);
    +
         this.widget = this.container.querySelector('a');
         this.icon = this.container.querySelector('#widgetIcon');
         this.label = this.container.querySelector('.js-sw__label');
    @@ -49,33 +74,32 @@ define(['./summary-widget.html', '@braintree/sanitize-url'], function (
     
         const renderTracker = {};
         this.renderTracker = renderTracker;
    +
         this.openmct.telemetry
           .request(this.domainObject, {
             strategy: 'latest',
             size: 1
           })
    -      .then(
    -        function (results) {
    -          if (
    -            this.destroyed ||
    -            this.hasUpdated ||
    -            this.renderTracker !== renderTracker ||
    -            results.length === 0
    -          ) {
    -            return;
    -          }
    -
    -          this.updateState(results[results.length - 1]);
    -        }.bind(this)
    -      );
    +      .then((results) => {
    +        if (
    +          this.destroyed ||
    +          this.hasUpdated ||
    +          this.renderTracker !== renderTracker ||
    +          results.length === 0
    +        ) {
    +          return;
    +        }
    +
    +        this.updateState(results[results.length - 1]);
    +      });
     
         this.unsubscribe = this.openmct.telemetry.subscribe(
           this.domainObject,
           this.updateState.bind(this)
         );
    -  };
    +  }
     
    -  SummaryWidgetView.prototype.show = function (container) {
    +  show(container) {
         this.container = container;
         this.render();
         this.removeMutationListener = this.openmct.objects.observe(
    @@ -84,14 +108,14 @@ define(['./summary-widget.html', '@braintree/sanitize-url'], function (
           this.onMutation.bind(this)
         );
         this.openmct.time.on('timeSystem', this.render);
    -  };
    +  }
     
    -  SummaryWidgetView.prototype.onMutation = function (domainObject) {
    +  onMutation(domainObject) {
         this.domainObject = domainObject;
         this.render();
    -  };
    +  }
     
    -  SummaryWidgetView.prototype.destroy = function (container) {
    +  destroy() {
         this.unsubscribe();
         this.removeMutationListener();
         this.openmct.time.off('timeSystem', this.render);
    @@ -100,7 +124,7 @@ define(['./summary-widget.html', '@braintree/sanitize-url'], function (
         delete this.label;
         delete this.openmct;
         delete this.domainObject;
    -  };
    +  }
    +}
     
    -  return SummaryWidgetView;
    -});
    +export default SummaryWidgetView;
    
  • src/utils/template/templateHelpers.js+12 3 modified
    @@ -1,8 +1,17 @@
     export function convertTemplateToHTML(templateString) {
    -  const template = document.createElement('template');
    -  template.innerHTML = templateString;
    +  const parser = new DOMParser();
    +  const doc = parser.parseFromString(templateString, 'text/html');
     
    -  return template.content.cloneNode(true).children;
    +  // Create a document fragment to hold the parsed content
    +  const fragment = document.createDocumentFragment();
    +
    +  // Append nodes from the parsed content to the fragment
    +  while (doc.body.firstChild) {
    +    fragment.appendChild(doc.body.firstChild);
    +  }
    +
    +  // Convert children of the fragment to an array and return
    +  return Array.from(fragment.children);
     }
     
     export function toggleClass(element, className) {
    

Vulnerability mechanics

Root cause

"Unsanitized user-controlled data is injected into the DOM via `.innerHTML` assignments, allowing an attacker to execute arbitrary JavaScript in the context of a victim's session."

Attack vector

An attacker can craft a malicious payload containing JavaScript and embed it in a domain object name, rule label, or other user-controlled property that is rendered via `.innerHTML` in the flexibleLayout or summaryWidget plugins. When a victim with an active Open MCT session views or interacts with the crafted object (e.g., by dragging a frame in the flexible layout), the payload executes in the victim's browser context. This allows the attacker to forge requests on behalf of the victim, bypassing CSRF protections because the browser automatically includes session cookies with same-origin requests [CWE-352].

Affected code

The patch touches multiple files across the Open MCT codebase, primarily in `src/plugins/summaryWidget/src/views/SummaryWidgetView.js`, `src/plugins/summaryWidget/src/Rule.js`, `src/plugins/summaryWidget/src/Condition.js`, `src/plugins/summaryWidget/src/input/Palette.js`, `src/plugins/summaryWidget/src/SummaryWidget.js`, `src/plugins/flexibleLayout/components/Frame.vue`, `src/plugins/plot/chart/MctChart.vue`, and `src/utils/template/templateHelpers.js`. The core defect is the widespread use of `.innerHTML` assignments that inject unsanitized user-controlled data (such as `datum.ruleLabel`, `self.config.name`, `self.config.description`, and `this.domainObject.name`) into the DOM, enabling Cross-Site Scripting (XSS) and, by extension, Cross-Site Request Forgery (CSRF) via the flexibleLayout plugin.

What the fix does

The patch replaces all `.innerHTML` assignments with safe DOM manipulation methods such as `.textContent`, `.innerText`, `.appendChild()`, and `document.createElement()`. For example, in `SummaryWidgetView.js`, `this.label.innerHTML = datum.ruleLabel` becomes `this.label.textContent = datum.ruleLabel`, and in `Frame.vue`, `this.dragGhost.innerHTML = '

Preconditions

  • inputAttacker must have the ability to create or modify a domain object (e.g., a summary widget or flexible layout frame) with a crafted name, label, or description containing JavaScript.
  • authVictim must have an active authenticated session in Open MCT and must view or interact with the malicious object (e.g., drag a frame in flexible layout).
  • configThe application must not have additional CSRF tokens or origin validation on the targeted sensitive endpoints.

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

References

5

News mentions

0

No linked articles in our index yet.