From bfc39542aabbc5d3e9709ee34e324666dd7a8dec Mon Sep 17 00:00:00 2001 From: Pierre Donias Date: Tue, 27 May 2025 14:25:39 +0200 Subject: [PATCH] feat(xo-server,xo-web): show alert on VMs that are vulnerable to XSA-468 (#8638) See https://xenbits.xen.org/xsa/advisory-468.html See https://docs.xcp-ng.org/vms/#xsa-468-multiple-windows-pv-driver-vulnerabilities See https://xcp-ng.org/blog/xsa-468-windows-pv-driver-vulnerabilities/ --- @vates/types/src/xo.mts | 1 + CHANGELOG.unreleased.md | 11 ++- packages/xo-server/package.json | 1 + packages/xo-server/src/xapi-object-to-xo.mjs | 78 ++++++++++++++++++++ packages/xo-web/src/xo-app/home/index.js | 1 + packages/xo-web/src/xo-app/home/vm-item.js | 57 ++++++++++++++ packages/xo-web/src/xo-app/index.js | 25 ++++++- 7 files changed, 166 insertions(+), 8 deletions(-) diff --git a/@vates/types/src/xo.mts b/@vates/types/src/xo.mts index eb8809b755..d781ecddd8 100644 --- a/@vates/types/src/xo.mts +++ b/@vates/types/src/xo.mts @@ -106,6 +106,7 @@ type BaseXoVm = BaseXapiXo & { videoram?: number viridian: boolean virtualizationMode: DOMAIN_TYPE + vulnerabilities: { xsa468: boolean | { reason: string; driver?: string; version?: string } } xenStoreData: Record /** * @deprecated use pvDriversVersion instead diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index e1e1253bd2..7ecbebd3ed 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -7,6 +7,8 @@ > Security fixes and new features should go in this section +- [VM] Detect XSA-468 vulnerable VMs. Read our announcement on the [XCP-ng blog](https://xcp-ng.org/blog/xsa-468-windows-pv-driver-vulnerabilities) for more details. + ### Enhancements > Users must be able to say: “Nice enhancement, I'm eager to test it” @@ -23,10 +25,11 @@ > Users must be able to say: “I had this issue, happy to know it's fixed” - **XO 6**: + - [VM] Add auto redirection from /vm/[id] to /vm/[id]/console (PR [#8553](https://github.com/vatesfr/xen-orchestra/pull/8553)) - - [Hosts] Avoid getting XO tasks logs flooded with errors on `host.isPubKeyTooShort` (PR [#8605](https://github.com/vatesfr/xen-orchestra/pull/8605)) - - [VM] Fix "an error has occurred" in Advanced tab when VTPM is `null` (PR [#8601](https://github.com/vatesfr/xen-orchestra/pull/8601)) +- [Hosts] Avoid getting XO tasks logs flooded with errors on `host.isPubKeyTooShort` (PR [#8605](https://github.com/vatesfr/xen-orchestra/pull/8605)) +- [VM] Fix "an error has occurred" in Advanced tab when VTPM is `null` (PR [#8601](https://github.com/vatesfr/xen-orchestra/pull/8601)) ### Packages to release @@ -50,7 +53,7 @@ - @xen-orchestra/rest-api minor - @xen-orchestra/web patch - @xen-orchestra/web-core patch -- xo-server patch -- xo-web patch +- xo-server minor +- xo-web minor diff --git a/packages/xo-server/package.json b/packages/xo-server/package.json index 4553dd307d..ec92f826ec 100644 --- a/packages/xo-server/package.json +++ b/packages/xo-server/package.json @@ -67,6 +67,7 @@ "blocked-at": "^1.2.0", "bluebird": "^3.5.1", "body-parser": "^1.20.0", + "compare-versions": "^6.1.1", "complex-matcher": "^0.7.1", "compression": "^1.7.3", "connect-flash": "^0.1.1", diff --git a/packages/xo-server/src/xapi-object-to-xo.mjs b/packages/xo-server/src/xapi-object-to-xo.mjs index a360e004ac..e6db5936c8 100644 --- a/packages/xo-server/src/xapi-object-to-xo.mjs +++ b/packages/xo-server/src/xapi-object-to-xo.mjs @@ -5,6 +5,7 @@ import * as xoData from '@xen-orchestra/xapi/xoData.mjs' import ensureArray from './_ensureArray.mjs' import normalizeVmNetworks from './_normalizeVmNetworks.mjs' import semver from 'semver' +import { compareVersions, validate as validateVersion } from 'compare-versions' import { createLogger } from '@xen-orchestra/log' import { extractIpFromVmNetworks } from './_extractIpFromVmNetworks.mjs' import { extractProperty, forEach, isEmpty, mapFilter, parseXml } from './utils.mjs' @@ -93,6 +94,82 @@ const getVmGuestToolsProps = vm => { } } +// ***** May 2025 - Xen Security Advisory XSA-468 - Windows PV drivers vulnerability ***** + +// Vendor → (driver → version) +const XSA468_VULNERABLE_VERSIONS = { + Xen_Project: { + xencons: '9.1.0.2', + xeniface: '9.1.0.0', + xenbus: '9.1.0.1', + }, + Amazon_Inc_: { + xeniface: '8.3.0', + }, + XenServer: { + xeniface: '9.1.12.93', + xenbus: '9.1.11.114', + }, + Citrix: { + xeniface: '9.1.1.11', + xenbus: '9.1.2.14', + }, + XCP_ng: { + xencons: '9.0.9048.9047', + xeniface: '9.0.9048.9047', + xenbus: '9.0.9048.9047', + }, +} +const isVmVulnerable_XSA468 = vm => { + const guestMetrics = vm.$guest_metrics + + if (vm.platform?.device_id !== '0002') { + // Not a Windows VM + return false + } + + if (!guestMetrics.PV_drivers_detected) { + // No PV drivers installed: no vulnerability + return false + } + + const pvDriversVersion = guestMetrics.PV_drivers_version + let versionDetected = false + for (const [key, value] of Object.entries(pvDriversVersion)) { + if (['major', 'minor', 'micro', 'build'].includes(key)) { + continue + } + + const [vendor, version] = value.split(' ') + if (!validateVersion(version)) { + warn(`Invalid version number for ${vendor}: ${key} ${version}`) + continue + } + + const vendorVulnerableVersion = XSA468_VULNERABLE_VERSIONS[vendor]?.[key] + if (vendorVulnerableVersion === undefined) { + continue + } + + versionDetected = true + + try { + if (compareVersions(version, vendorVulnerableVersion) <= 0) { + // PV drivers installed and vulnerable version detected + return { reason: 'pv-driver-version-vulnerable', driver: key, version } + } + } catch (err) { + warn(err) + } + } + + // - PV drivers installed and could check safe versions: no vulnerability + // - PV drivers installed but could not check versions: potential vulnerability + return versionDetected ? false : { reason: 'no-pv-drivers-detected' } +} + +// *************************************************************************************** + // =================================================================== const TRANSFORMS = { @@ -405,6 +482,7 @@ const TRANSFORMS = { isNestedVirtEnabled: semver.satisfies(String(obj.$pool.$master.software_version.platform_version), '>=3.4') ? obj.platform['nested-virt'] === 'true' : obj.platform['exp-nested-hvm'] === 'true', + vulnerabilities: { xsa468: isVmVulnerable_XSA468(obj) }, viridian: obj.platform.viridian === 'true', mainIpAddress: extractIpFromVmNetworks(guestMetrics?.networks), high_availability: obj.ha_restart_priority, diff --git a/packages/xo-web/src/xo-app/home/index.js b/packages/xo-web/src/xo-app/home/index.js index b851172db4..65a3780c74 100644 --- a/packages/xo-web/src/xo-app/home/index.js +++ b/packages/xo-web/src/xo-app/home/index.js @@ -496,6 +496,7 @@ const NoObjects = props => } return acc }, []), + vulnerable: Object.values(vm.vulnerabilities).filter(Boolean).length > 0, }, { container: { value: containers[vm.$container || vm.$pool] }, diff --git a/packages/xo-web/src/xo-app/home/vm-item.js b/packages/xo-web/src/xo-app/home/vm-item.js index fb9e30d3b0..888656a05c 100644 --- a/packages/xo-web/src/xo-app/home/vm-item.js +++ b/packages/xo-web/src/xo-app/home/vm-item.js @@ -1,4 +1,5 @@ import _ from 'intl' +import BulkIcons from 'bulk-icons' import Component from 'base-component' import Ellipsis, { EllipsisContainer } from 'ellipsis' import Icon from 'icon' @@ -65,6 +66,60 @@ export default class VmItem extends Component { (powerState, operations) => (!isEmpty(operations) ? 'Busy' : powerState) ) + _getAlerts = createSelector( + () => this.props.item, + vm => { + const alerts = [] + + if (vm.vulnerabilities.xsa468) { + const { reason, driver, version } = vm.vulnerabilities.xsa468 + + if (reason === 'no-pv-drivers-detected') { + alerts.push({ + level: 'warning', + render: ( +

+ + We cannot detect the version of Windows PV drivers on this VM. You may be running an outdated version. + Check XCP-ng's{' '} + + Windows Guest Tools Security documentation + {' '} + for more details. + +
+
+ Still seeing this message even though you updated PV drivers? Please update your XCP-ng. +

+ ), + }) + } else { + alerts.push({ + level: 'danger', + render: ( +

+ + This VM is running a Windows PV driver vulnerable to XSA-468 ({driver} {version}). You must upgrade + your Windows PV drivers now. See{' '} + + XCP-ng's documentation + {' '} + for more details. + +

+ ), + }) + } + } + + return alerts + } + ) + render() { const { item: vm, container, expandAll, isAdmin, selected } = this.props const resourceSet = this._getResourceSet() @@ -103,6 +158,8 @@ export default class VmItem extends Component { useLongClick /> +   + diff --git a/packages/xo-web/src/xo-app/index.js b/packages/xo-web/src/xo-app/index.js index d7ba9cacb6..2bb817eaa4 100644 --- a/packages/xo-web/src/xo-app/index.js +++ b/packages/xo-web/src/xo-app/index.js @@ -55,7 +55,13 @@ import Import from './import' import keymap, { help } from '../keymap' import Tooltip from '../common/tooltip' import { createCollectionWrapper, createGetObjectsOfType } from '../common/selectors' -import { bindXcpngLicense, rebindLicense, subscribeXcpngLicenses, subscribeXostorLicenses, subscribeSelfLicenses } from '../common/xo' +import { + bindXcpngLicense, + rebindLicense, + subscribeXcpngLicenses, + subscribeXostorLicenses, + subscribeSelfLicenses, +} from '../common/xo' import { SOURCES } from '../common/xoa-plans' import { getLicenseNearExpiration } from '../common/xoa-updater' @@ -140,16 +146,20 @@ export const ICON_POOL_LICENSE = { @addSubscriptions({ xcpLicenses: subscribeXcpngLicenses, xostorLicenses: subscribeXostorLicenses, - selfLicences: subscribeSelfLicenses + selfLicences: subscribeSelfLicenses, }) @connectStore(state => { const getHosts = createGetObjectsOfType('host') const getXostors = createGetObjectsOfType('SR').filter([sr => sr.SR_type === 'linstor']) + const getXsa468VulnerableVms = createGetObjectsOfType('VM').filter([ + vm => vm.vulnerabilities?.xsa468?.reason === 'pv-driver-version-vulnerable', + ]) return { trial: state.xoaTrialState, registerNeeded: state.xoaUpdaterState === 'registerNeeded', signedUp: !!state.user, hosts: getHosts(state), + xsa468VulnerableVms: getXsa468VulnerableVms(state), xostors: getXostors(state), } }) @@ -314,7 +324,7 @@ export const ICON_POOL_LICENSE = { }) return xostorLicenseInfoByXostorId - } + }, }, }) export default class XoApp extends Component { @@ -439,7 +449,7 @@ export default class XoApp extends Component { } render() { - const { signedUp, trial, registerNeeded } = this.props + const { signedUp, trial, registerNeeded, xsa468VulnerableVms } = this.props const { pathname } = this.context.router.location const licenseNearExpiration = this.props.selfLicences && getLicenseNearExpiration(this.props.selfLicences, trial) // If we are under expired or unstable trial (signed up only) @@ -448,6 +458,7 @@ export default class XoApp extends Component { (blockXoaAccess(trial) || licenseNearExpiration?.blocked === true) && !(pathname.startsWith('/xoa/') || pathname === '/backup/restore') const plan = getXoaPlan() + return ( @@ -502,6 +513,12 @@ export default class XoApp extends Component { })} )} + {Object.keys(xsa468VulnerableVms).length > 0 && ( +
+ IMPORTANT! Some of your VMs are vulnerable.{' '} + Please check immediately. +
+ )}