From b91b673a00b3457b32d2001d8010ced61598a5d1 Mon Sep 17 00:00:00 2001 From: Artem Chernyshev Date: Tue, 18 Mar 2025 22:14:06 +0300 Subject: [PATCH] fix: add more strict security headers to the web page handler Enable `CSP`, `Referrer-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, `Permissions-Policy` headers. `CSP` has broken the monaco editor, so had to drop the monaco vite plugin for production builds. Signed-off-by: Artem Chernyshev --- frontend/package.json | 48 +++++++++---------- .../common/CodeEditor/CodeEditor.vue | 4 ++ frontend/src/main.ts | 19 +++++++- frontend/src/methods/logs.ts | 6 ++- frontend/src/views/cluster/Nodes/NodeLogs.vue | 8 +++- frontend/vite.config.ts | 44 +++++++++-------- internal/frontend/handler.go | 21 +++++++- 7 files changed, 99 insertions(+), 51 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index c3bc0046..7c2ccc5a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,51 +10,51 @@ "lint": "eslint ./src" }, "dependencies": { - "@auth0/auth0-vue": "^2.3.3", + "@auth0/auth0-vue": "^2.4.0", "@headlessui/vue": "^1.7.23", - "@jsonforms/vue": "^3.4.1", - "@jsonforms/vue-vanilla": "^3.4.1", - "@kubernetes/client-node": "^0.22.2", + "@jsonforms/vue": "^3.5.1", + "@jsonforms/vue-vanilla": "^3.5.1", + "@kubernetes/client-node": "^0.22.3", "apexcharts": "3.45.2", "click-outside-vue3": "^4.0.1", - "core-js": "^3.39.0", + "core-js": "^3.41.0", "js-yaml": "^4.1.0", - "long": "^5.2.3", + "long": "^5.3.1", "luxon": "^3.5.0", - "monaco-editor": "^0.41.0", - "monaco-yaml": "^5.2.3", + "monaco-editor": "^0.52.2", + "monaco-yaml": "^5.3.1", "pluralize": "^8.0.0", - "rxjs": "^7.8.1", - "semver-parser": "^4.1.6", + "rxjs": "^7.8.2", + "semver-parser": "^4.1.8", "uuid": "^10.0.0", - "vue": "^3.5.12", - "vue-router": "^4.4.5", + "vue": "^3.5.13", + "vue-router": "^4.5.0", "vue-virtual-scroller": "^2.0.0-beta.8", "vue-word-highlighter": "^1.2.5", - "vue3-apexcharts": "^1.7.0", + "vue3-apexcharts": "^1.8.0", "vue3-popper": "^1.5.0" }, "devDependencies": { - "@heroicons/vue": "^2.1.5", + "@heroicons/vue": "^2.2.0", "@types/luxon": "^3.4.2", - "@types/node": "^20.17.6", + "@types/node": "^20.17.24", "@types/pluralize": "^0.0.33", - "@vitejs/plugin-vue": "^5.1.4", - "autoprefixer": "^10.4.20", + "@vitejs/plugin-vue": "^5.2.3", + "autoprefixer": "^10.4.21", "eslint-plugin-typescript": "^0.14.0", - "eslint-plugin-vue": "^9.30.0", + "eslint-plugin-vue": "^9.33.0", "fetch-intercept": "^2.4.0", "jsdom": "^25.0.1", "lodash": "^4.17.21", "openpgp": "^5.11.2", - "postcss": "^8.4.47", - "tailwindcss": "^3.4.14", - "typescript": "^5.6.3", - "typescript-eslint": "^8.13.0", - "vite": "^5.4.10", + "postcss": "^8.5.3", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.2", + "typescript-eslint": "^8.26.1", + "vite": "^5.4.14", "vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-node-polyfills": "^0.22.0", - "vue-tsc": "^2.1.10", + "vue-tsc": "^2.2.8", "vue3-clipboard": "^1.0.0", "whatwg-fetch": "^3.6.20" } diff --git a/frontend/src/components/common/CodeEditor/CodeEditor.vue b/frontend/src/components/common/CodeEditor/CodeEditor.vue index 05c3a884..07d3c74c 100644 --- a/frontend/src/components/common/CodeEditor/CodeEditor.vue +++ b/frontend/src/components/common/CodeEditor/CodeEditor.vue @@ -201,4 +201,8 @@ monaco.editor.defineTheme("sidero", { .editor h4 { @apply font-bold; } + +.monaco-editor { + outline: 0; +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 9ba45cdc..7afe4c0a 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -10,6 +10,7 @@ import VueClipboard from 'vue3-clipboard'; import AppUnavailable from '@/AppUnavailable.vue' import router from '@/router'; + import { initState, ResourceService, Resource } from "@/api/grpc"; import { AuthConfigID, AuthConfigType, DefaultNamespace } from "@/api/resources"; import { Runtime } from "@/api/common/omni.pb"; @@ -17,7 +18,21 @@ import { AuthConfigSpec } from "@/api/omni/specs/auth.pb"; import { AuthType, authType, suspended } from "@/methods"; import { createAuth0 } from "@auth0/auth0-vue"; import { withRuntime } from "./api/options"; -import vClickOutside from "click-outside-vue3" +import vClickOutside from "click-outside-vue3"; +import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import yamlWorker from 'monaco-yaml/yaml.worker?worker'; + +if (process.env.NODE_ENV !== 'development') { + (self as any).MonacoEnvironment = { + getWorker(_: string, label: string) { + if (label === 'yaml') { + return new yamlWorker(); + } + + return new editorWorker(); + } + }; +} const setupApp = async () => { let authConfigSpec: AuthConfigSpec | undefined = undefined @@ -68,4 +83,4 @@ const setupApp = async () => { initState(); -setupApp() +setupApp(); diff --git a/frontend/src/methods/logs.ts b/frontend/src/methods/logs.ts index 72db4f8a..b1a34a6c 100644 --- a/frontend/src/methods/logs.ts +++ b/frontend/src/methods/logs.ts @@ -75,7 +75,7 @@ export class LineDelimitedLogParser { } } -export const setupLogStream = (logs: Ref, method: StreamingRequest, params: T | ComputedRef | Ref, logParser: LogParser = new DefaultLogParser((l: string): LogLine => { return {msg: l} }), ...options: fetchOption[]): Ref | undefined> => { +export const setupLogStream = (logs: Ref, method: StreamingRequest, params: T | ComputedRef | Ref, logParser: LogParser = new DefaultLogParser((l: string): LogLine => { return {msg: l} }), ...options: fetchOption[]): Ref | undefined> => { const stream: Ref | undefined> = ref(); let buffer: LogLine[] = []; let flush: NodeJS.Timeout | undefined; @@ -97,6 +97,10 @@ export const setupLogStream = (logs: Ref, method: const p = isRef(params) ? params.value : params; + if (!p) { + return; + } + stream.value = subscribe( method, p, diff --git a/frontend/src/views/cluster/Nodes/NodeLogs.vue b/frontend/src/views/cluster/Nodes/NodeLogs.vue index 66bd2f39..a4e2f274 100644 --- a/frontend/src/views/cluster/Nodes/NodeLogs.vue +++ b/frontend/src/views/cluster/Nodes/NodeLogs.vue @@ -108,7 +108,11 @@ const plainText = (line: string) => { }; } -const params = computed(() => { +const params = computed(() => { + if (route.params.service === "machine") { + return; + } + return { namespace: "system", id: service.value, @@ -134,7 +138,7 @@ watch(() => route.params.service, () => { logParser.setLineParser(getLineParser(svc)); service.value = svc; -}) +}); const stream = setupLogStream(logs, MachineService.Logs, params, logParser, withRuntime(Runtime.Talos), withContext(context)); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 50971f59..8232c081 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -5,7 +5,7 @@ /// -import { defineConfig } from 'vite' +import { defineConfig, UserConfig } from 'vite' import { fileURLToPath, URL } from 'node:url' import { nodePolyfills } from 'vite-plugin-node-polyfills' import monacoEditorPlugin from 'vite-plugin-monaco-editor' @@ -13,11 +13,25 @@ import monacoEditorPlugin from 'vite-plugin-monaco-editor' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ -export default defineConfig({ - plugins: [ - vue(), - nodePolyfills({ include: ['stream'] }), - monacoEditorPlugin({ +export default defineConfig(({ command }) => { + const config: UserConfig = { + plugins: [ + vue(), + nodePolyfills({ include: ['stream'] }), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + } + }, + server: { + port: 8121, + host: "127.0.0.1" + }, + }; + + if (command === 'serve') { + config.plugins?.push(monacoEditorPlugin({ languageWorkers: ['editorWorkerService'], customWorkers: [ { @@ -25,18 +39,8 @@ export default defineConfig({ entry: 'monaco-yaml/yaml.worker' } ] - }) - ], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)), - } - }, - server: { - port: 8121, - host: "127.0.0.1" - }, - test: { - exclude: [], - }, + })); + } + + return config; }) diff --git a/internal/frontend/handler.go b/internal/frontend/handler.go index 5dc1eb90..63ee0b3f 100644 --- a/internal/frontend/handler.go +++ b/internal/frontend/handler.go @@ -102,12 +102,29 @@ func (handler *StaticHandler) serveFile(w http.ResponseWriter, r *http.Request, return } + defer file.Close() //nolint:errcheck + if path != index { w.Header().Set("Vary", "Accept-Encoding, User-Agent") w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, immutable", handler.maxAgeSec)) - } + } else { + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") - defer file.Close() //nolint:errcheck + w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src * data: ; "+ + ";connect-src 'self' https://*.auth0.com ;font-src 'self' data: "+ + ";style-src 'self' 'unsafe-inline' https://fonts.googleapis.com data: ;upgrade-insecure-requests;"+ + ";frame-src https://*.auth0.com", + ) + + w.Header().Set("X-Frame-Options", "SAMEORIGIN") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Permissions-Policy", "accelerometer=(), ambient-light-sensor=(), "+ + "autoplay=(self), battery=(), camera=(), cross-origin-isolated=(self), display-capture=(), "+ + "document-domain=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), "+ + "magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials=(self),"+ + "screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=()", + ) + } http.ServeContent(w, r, file.Name(), handler.modTime, file)