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.
| Package | Affected versions | Patched versions |
|---|---|---|
openmctnpm | < 3.1.1 | 3.1.1 |
Affected products
2- NASA/Open MCTdescription
Patches
1ff8288d10ec7cherry-pick(#7144): fix(#7143): add `eslint-plugin-no-unsanitized` and fix errors (#7148)
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- github.com/advisories/GHSA-4g88-4hgm-m99xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-45884ghsaADVISORY
- github.com/nasa/openmct/pull/7148ghsaWEB
- github.com/nasa/openmct/pull/7148/commits/4e95e12559c9c5364269ff366a59768573baacb4ghsaWEB
- www.linkedin.com/pulse/xss-nasas-open-mct-v302-visionspace-technologies-ubg4fghsaWEB
News mentions
0No linked articles in our index yet.