diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e8955661..e25760e70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the Wazuh app project will be documented in this file. ### Added - Support for Wazuh 4.7.5 +- Added sanitization to custom branding SVG files [#6687](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6687) ## Wazuh v4.7.4 - OpenSearch Dashboards 2.8.0 - Revision 02 diff --git a/plugins/main/package.json b/plugins/main/package.json index 89da1b34bb..0f7fc0dacb 100644 --- a/plugins/main/package.json +++ b/plugins/main/package.json @@ -63,7 +63,9 @@ "read-last-lines": "^1.7.2", "timsort": "^0.3.0", "typescript": "^5.0.4", - "winston": "3.9.0" + "winston": "3.9.0", + "dompurify": "^3.1.3", + "jsdom": "16.7.0" }, "devDependencies": { "@types/node-cron": "^2.0.3", diff --git a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts index dfaba3b656..be5a566ee9 100644 --- a/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts +++ b/plugins/main/server/controllers/wazuh-utils/wazuh-utils.ts @@ -16,9 +16,17 @@ import { getConfiguration } from '../../lib/get-configuration'; import { read } from 'read-last-lines'; import { UpdateConfigurationFile } from '../../lib/update-configuration'; import jwtDecode from 'jwt-decode'; -import { WAZUH_ROLE_ADMINISTRATOR_ID, WAZUH_DATA_LOGS_RAW_PATH, PLUGIN_SETTINGS } from '../../../common/constants'; +import { + WAZUH_ROLE_ADMINISTRATOR_ID, + WAZUH_DATA_LOGS_RAW_PATH, + PLUGIN_SETTINGS, +} from '../../../common/constants'; import { ManageHosts } from '../../lib/manage-hosts'; -import { OpenSearchDashboardsRequest, RequestHandlerContext, OpenSearchDashboardsResponseFactory } from 'src/core/server'; +import { + OpenSearchDashboardsRequest, + RequestHandlerContext, + OpenSearchDashboardsResponseFactory, +} from 'src/core/server'; import { getCookieValueByName } from '../../lib/cookie'; import fs from 'fs'; import path from 'path'; @@ -26,6 +34,8 @@ import { createDirectoryIfNotExists } from '../../lib/filesystem'; import glob from 'glob'; import { getFileExtensionFromBuffer } from '../../../common/services/file-extension'; +import { sanitizeSVG } from '../../lib/sanitizer'; + const updateConfigurationFile = new UpdateConfigurationFile(); // TODO: these controllers have no logs. We should include them. @@ -45,7 +55,11 @@ export class WazuhUtilsCtrl { * @param {Object} response * @returns {Object} Configuration File or ErrorResponse */ - getConfigurationFile(context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory) { + getConfigurationFile( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + ) { try { const configFile = getConfiguration(); @@ -53,8 +67,8 @@ export class WazuhUtilsCtrl { body: { statusCode: 200, error: 0, - data: configFile || {} - } + data: configFile || {}, + }, }); } catch (error) { return ErrorResponse(error.message || error, 3019, 500, response); @@ -68,35 +82,74 @@ export class WazuhUtilsCtrl { * @param {Object} response * @returns {Object} Configuration File or ErrorResponse */ - updateConfigurationFile = this.routeDecoratorProtectedAdministratorRoleValidToken( - async (context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory) => { - - let requiresRunningHealthCheck: boolean = false, - requiresReloadingBrowserTab: boolean = false, - requiresRestartingPluginPlatform: boolean = false; - - // Plugin settings configurables in the configuration file. - const pluginSettingsConfigurableFile = Object.keys(request.body) - .filter(pluginSettingKey => PLUGIN_SETTINGS[pluginSettingKey].isConfigurableFromFile) - .reduce((accum, pluginSettingKey: string) => ({ ...accum, [pluginSettingKey]: request.body[pluginSettingKey] }), {}); + updateConfigurationFile = + this.routeDecoratorProtectedAdministratorRoleValidToken( + async ( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + ) => { + let requiresRunningHealthCheck: boolean = false, + requiresReloadingBrowserTab: boolean = false, + requiresRestartingPluginPlatform: boolean = false; - if (Object.keys(pluginSettingsConfigurableFile).length) { - // Update the configuration file. - await updateConfigurationFile.updateConfiguration(pluginSettingsConfigurableFile); + // Plugin settings configurables in the configuration file. + const pluginSettingsConfigurableFile = Object.keys(request.body) + .filter( + pluginSettingKey => + PLUGIN_SETTINGS[pluginSettingKey].isConfigurableFromFile, + ) + .reduce( + (accum, pluginSettingKey: string) => ({ + ...accum, + [pluginSettingKey]: request.body[pluginSettingKey], + }), + {}, + ); - requiresRunningHealthCheck = Object.keys(pluginSettingsConfigurableFile).some((pluginSettingKey: string) => Boolean(PLUGIN_SETTINGS[pluginSettingKey].requiresRunningHealthCheck)) || requiresRunningHealthCheck; - requiresReloadingBrowserTab = Object.keys(pluginSettingsConfigurableFile).some((pluginSettingKey: string) => Boolean(PLUGIN_SETTINGS[pluginSettingKey].requiresReloadingBrowserTab)) || requiresReloadingBrowserTab; - requiresRestartingPluginPlatform = Object.keys(pluginSettingsConfigurableFile).some((pluginSettingKey: string) => Boolean(PLUGIN_SETTINGS[pluginSettingKey].requiresRestartingPluginPlatform)) || requiresRestartingPluginPlatform; - }; + if (Object.keys(pluginSettingsConfigurableFile).length) { + // Update the configuration file. + await updateConfigurationFile.updateConfiguration( + pluginSettingsConfigurableFile, + ); - return response.ok({ - body: { - data: { requiresRunningHealthCheck, requiresReloadingBrowserTab, requiresRestartingPluginPlatform, updatedConfiguration: pluginSettingsConfigurableFile } + requiresRunningHealthCheck = + Object.keys(pluginSettingsConfigurableFile).some( + (pluginSettingKey: string) => + Boolean( + PLUGIN_SETTINGS[pluginSettingKey].requiresRunningHealthCheck, + ), + ) || requiresRunningHealthCheck; + requiresReloadingBrowserTab = + Object.keys(pluginSettingsConfigurableFile).some( + (pluginSettingKey: string) => + Boolean( + PLUGIN_SETTINGS[pluginSettingKey].requiresReloadingBrowserTab, + ), + ) || requiresReloadingBrowserTab; + requiresRestartingPluginPlatform = + Object.keys(pluginSettingsConfigurableFile).some( + (pluginSettingKey: string) => + Boolean( + PLUGIN_SETTINGS[pluginSettingKey] + .requiresRestartingPluginPlatform, + ), + ) || requiresRestartingPluginPlatform; } - }); - }, - 3021 - ) + + return response.ok({ + body: { + data: { + requiresRunningHealthCheck, + requiresReloadingBrowserTab, + requiresRestartingPluginPlatform, + updatedConfiguration: pluginSettingsConfigurableFile, + }, + }, + }); + }, + 3021, + ); /** * Returns Wazuh app logs @@ -105,22 +158,23 @@ export class WazuhUtilsCtrl { * @param {Object} response * @returns {Array} app logs or ErrorResponse */ - async getAppLogs(context: RequestHandlerContext, request: OpenSearchDashboardsRequest, response: OpenSearchDashboardsResponseFactory) { + async getAppLogs( + context: RequestHandlerContext, + request: OpenSearchDashboardsRequest, + response: OpenSearchDashboardsResponseFactory, + ) { try { - const lastLogs = await read( - WAZUH_DATA_LOGS_RAW_PATH, - 50 - ); + const lastLogs = await read(WAZUH_DATA_LOGS_RAW_PATH, 50); const spliterLog = lastLogs.split('\n'); return spliterLog && Array.isArray(spliterLog) ? response.ok({ - body: { - error: 0, - lastLogs: spliterLog.filter( - item => typeof item === 'string' && item.length - ) - } - }) + body: { + error: 0, + lastLogs: spliterLog.filter( + item => typeof item === 'string' && item.length, + ), + }, + }) : response.ok({ error: 0, lastLogs: [] }); } catch (error) { return ErrorResponse(error.message || error, 3036, 500, response); @@ -135,25 +189,44 @@ export class WazuhUtilsCtrl { * @returns {Object} Configuration File or ErrorResponse */ uploadFile = this.routeDecoratorProtectedAdministratorRoleValidToken( - async (context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) => { + async ( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory, + ) => { const { key } = request.params; - const { file: bufferFile } = request.body; + let { file: bufferFile } = request.body; const pluginSetting = PLUGIN_SETTINGS[key]; // Check file extension const fileExtension = getFileExtensionFromBuffer(bufferFile); // Check if the extension is valid for the setting. - if (!pluginSetting.options.file.extensions.includes(`.${fileExtension}`)) { + if ( + !pluginSetting.options.file.extensions.includes(`.${fileExtension}`) + ) { return response.badRequest({ - body: `File extension is not valid for setting [${key}] setting. Allowed file extensions: ${pluginSetting.options.file.extensions.join(', ')}` + body: `File extension is not valid for setting [${key}] setting. Allowed file extensions: ${pluginSetting.options.file.extensions.join( + ', ', + )}`, }); - }; + } + + // Sanitize SVG content to prevent prevents XSS attacks + if (fileExtension === 'svg') { + const svgString = bufferFile.toString(); + const cleanSVG = sanitizeSVG(svgString); + bufferFile = Buffer.from(cleanSVG); + } const fileNamePath = `${key}.${fileExtension}`; // Create target directory - const targetDirectory = path.join(__dirname, '../../..', pluginSetting.options.file.store.relativePathFileSystem); + const targetDirectory = path.join( + __dirname, + '../../..', + pluginSetting.options.file.store.relativePathFileSystem, + ); createDirectoryIfNotExists(targetDirectory); // Get the files related to the setting and remove them const files = glob.sync(path.join(targetDirectory, `${key}.*`)); @@ -163,24 +236,33 @@ export class WazuhUtilsCtrl { fs.writeFileSync(path.join(targetDirectory, fileNamePath), bufferFile); // Update the setting in the configuration cache - const pluginSettingValue = pluginSetting.options.file.store.resolveStaticURL(fileNamePath); - await updateConfigurationFile.updateConfiguration({ [key]: pluginSettingValue }); + const pluginSettingValue = + pluginSetting.options.file.store.resolveStaticURL(fileNamePath); + await updateConfigurationFile.updateConfiguration({ + [key]: pluginSettingValue, + }); return response.ok({ body: { data: { - requiresRunningHealthCheck: Boolean(pluginSetting.requiresRunningHealthCheck), - requiresReloadingBrowserTab: Boolean(pluginSetting.requiresReloadingBrowserTab), - requiresRestartingPluginPlatform: Boolean(pluginSetting.requiresRestartingPluginPlatform), + requiresRunningHealthCheck: Boolean( + pluginSetting.requiresRunningHealthCheck, + ), + requiresReloadingBrowserTab: Boolean( + pluginSetting.requiresReloadingBrowserTab, + ), + requiresRestartingPluginPlatform: Boolean( + pluginSetting.requiresRestartingPluginPlatform, + ), updatedConfiguration: { - [key]: pluginSettingValue - } - } - } + [key]: pluginSettingValue, + }, + }, + }, }); }, - 3022 - ) + 3022, + ); /** * Delete a file @@ -190,64 +272,96 @@ export class WazuhUtilsCtrl { * @returns {Object} Configuration File or ErrorResponse */ deleteFile = this.routeDecoratorProtectedAdministratorRoleValidToken( - async (context: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory) => { + async ( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory, + ) => { const { key } = request.params; const pluginSetting = PLUGIN_SETTINGS[key]; // Get the files related to the setting and remove them - const targetDirectory = path.join(__dirname, '../../..', pluginSetting.options.file.store.relativePathFileSystem); + const targetDirectory = path.join( + __dirname, + '../../..', + pluginSetting.options.file.store.relativePathFileSystem, + ); const files = glob.sync(path.join(targetDirectory, `${key}.*`)); files.forEach(fs.unlinkSync); // Update the setting in the configuration cache const pluginSettingValue = pluginSetting.defaultValue; - await updateConfigurationFile.updateConfiguration({ [key]: pluginSettingValue }); + await updateConfigurationFile.updateConfiguration({ + [key]: pluginSettingValue, + }); return response.ok({ body: { - message: 'All files were removed and the configuration file was updated.', + message: + 'All files were removed and the configuration file was updated.', data: { - requiresRunningHealthCheck: Boolean(pluginSetting.requiresRunningHealthCheck), - requiresReloadingBrowserTab: Boolean(pluginSetting.requiresReloadingBrowserTab), - requiresRestartingPluginPlatform: Boolean(pluginSetting.requiresRestartingPluginPlatform), + requiresRunningHealthCheck: Boolean( + pluginSetting.requiresRunningHealthCheck, + ), + requiresReloadingBrowserTab: Boolean( + pluginSetting.requiresReloadingBrowserTab, + ), + requiresRestartingPluginPlatform: Boolean( + pluginSetting.requiresRestartingPluginPlatform, + ), updatedConfiguration: { - [key]: pluginSettingValue - } - } - } + [key]: pluginSettingValue, + }, + }, + }, }); }, - 3023 - ) + 3023, + ); - private routeDecoratorProtectedAdministratorRoleValidToken(routeHandler, errorCode: number) { + private routeDecoratorProtectedAdministratorRoleValidToken( + routeHandler, + errorCode: number, + ) { return async (context, request, response) => { try { // Check if user has administrator role in token const token = getCookieValueByName(request.headers.cookie, 'wz-token'); if (!token) { return ErrorResponse('No token provided', 401, 401, response); - }; + } const decodedToken = jwtDecode(token); if (!decodedToken) { return ErrorResponse('No permissions in token', 401, 401, response); - }; - if (!decodedToken.rbac_roles || !decodedToken.rbac_roles.includes(WAZUH_ROLE_ADMINISTRATOR_ID)) { + } + if ( + !decodedToken.rbac_roles || + !decodedToken.rbac_roles.includes(WAZUH_ROLE_ADMINISTRATOR_ID) + ) { return ErrorResponse('No administrator role', 401, 401, response); - }; + } // Check the provided token is valid - const apiHostID = getCookieValueByName(request.headers.cookie, 'wz-api'); + const apiHostID = getCookieValueByName( + request.headers.cookie, + 'wz-api', + ); if (!apiHostID) { return ErrorResponse('No API id provided', 401, 401, response); - }; - const responseTokenIsWorking = await context.wazuh.api.client.asCurrentUser.request('GET', '/', {}, { apiHostID }); + } + const responseTokenIsWorking = + await context.wazuh.api.client.asCurrentUser.request( + 'GET', + '/', + {}, + { apiHostID }, + ); if (responseTokenIsWorking.status !== 200) { return ErrorResponse('Token is not valid', 401, 401, response); - }; - return await routeHandler(context, request, response) + } + return await routeHandler(context, request, response); } catch (error) { return ErrorResponse(error.message || error, errorCode, 500, response); } - } + }; } } diff --git a/plugins/main/server/lib/sanitizer/index.ts b/plugins/main/server/lib/sanitizer/index.ts new file mode 100644 index 0000000000..3df0a0bd2f --- /dev/null +++ b/plugins/main/server/lib/sanitizer/index.ts @@ -0,0 +1 @@ +export { sanitizeSVG } from './sanitizer'; diff --git a/plugins/main/server/lib/sanitizer/sanitizer.test.ts b/plugins/main/server/lib/sanitizer/sanitizer.test.ts new file mode 100644 index 0000000000..0c19883660 --- /dev/null +++ b/plugins/main/server/lib/sanitizer/sanitizer.test.ts @@ -0,0 +1,47 @@ +import { sanitizeSVG } from './sanitizer'; + +const mockedFilesContents = [ + { + original: ` + + + + + +`, + clean: ` + + + + + +`, + }, + { original: '', clean: '' }, + { original: '', clean: '' }, + { + original: '

abc