OliveTin: Unauthenticated Denial of Service via Memory Exhaustion in PasswordHash API Endpoint
Description
OliveTin gives access to predefined shell commands from a web interface. Prior to version 3000.10.2, the PasswordHash API endpoint allows unauthenticated users to trigger excessive memory allocation by sending concurrent password hashing requests. By issuing multiple parallel requests, an attacker can exhaust available container memory, leading to service degradation or complete denial of service (DoS). The issue occurs because the endpoint performs computationally and memory-intensive hashing operations without request throttling, authentication requirements, or resource limits. This issue has been patched in version 3000.10.2.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
OliveTin versions before 3000.10.2 allow unauthenticated attackers to cause denial of service via memory exhaustion by sending concurrent password hashing requests to the PasswordHash API endpoint.
Vulnerability
Overview
OliveTin, a web interface for executing predefined shell commands, contains a denial-of-service vulnerability in its PasswordHash API endpoint. Prior to version 3000.10.2, this endpoint performs computationally and memory-intensive hashing operations without any request throttling, authentication requirements, or resource limits [1][3]. An unauthenticated attacker can exploit this by sending multiple concurrent requests, causing excessive memory allocation.
Exploitation
Details
The vulnerable endpoint is POST /api/olivetin.api.v1.OliveTinApiService/PasswordHash, which accepts a JSON body with a password field and returns a hash [3]. Because no authentication or rate limiting is enforced, an attacker can issue parallel requests from any network-accessible position. In a test environment, 50 concurrent requests consumed approximately 3.2 GB of memory (≈64 MB per request), leading to service instability [3].
Impact
Successful exploitation results in memory exhaustion, degrading or completely denying service to legitimate users. The attack does not require any prior authentication or special privileges, making it accessible to any unauthenticated adversary [1][3].
Mitigation
The issue has been patched in OliveTin version 3000.10.2, which introduces a 10-slot semaphore around password hash functions to limit concurrent operations [4]. Users are advised to upgrade immediately. No workarounds are documented; however, deploying the application behind a reverse proxy with rate limiting could provide partial mitigation.
AI Insight generated on May 18, 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 |
|---|---|---|
github.com/OliveTin/OliveTinGo | < 0.0.0-20260227002407-2eb5f0ba79d4 | 0.0.0-20260227002407-2eb5f0ba79d4 |
Affected products
2- OliveTin/OliveTinv5Range: < 3000.10.2
Patches
117 files changed · +491 −461
AGENTS.md+3 −0 modified@@ -19,6 +19,8 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`. - From repo root: `go run ./service` - Unit tests (Go): - From repo root: `cd service && make unittests` +- Code style (after editing code in `service/`): + - From repo root: `cd service && make codestyle` - Integration tests (Mocha + Selenium): - Single test: `cd integration-tests && npx --yes mocha test/general.mjs` - All tests: `cd integration-tests && npx --yes mocha` @@ -41,6 +43,7 @@ If you are looking for OliveTin's AI policy, you can find it in `AI.md`. - Do not swallow errors; propagate or log meaningfully. - Match existing formatting; avoid unrelated reformatting. - Be safe around nils in executor steps (e.g., guard `req.Binding` and `req.Binding.Action`). +- Cyclomatic complexity over 4 is not permitted. ### API and Execution Flow (High-level) 1. Client calls Connect RPC (e.g., `Init`, `GetDashboard`, `StartAction`).
frontend/package.json+4 −4 modified@@ -6,7 +6,7 @@ "source": "index.html", "devDependencies": { "process": "^0.11.10", - "stylelint": "^17.3.0", + "stylelint": "^17.4.0", "stylelint-config-standard": "^40.0.0" }, "scripts": { @@ -24,7 +24,7 @@ "dependencies": { "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-web": "^2.1.1", - "@hugeicons/core-free-icons": "^3.1.1", + "@hugeicons/core-free-icons": "^3.3.0", "@hugeicons/vue": "^1.0.4", "@vitejs/plugin-vue": "^6.0.4", "@xterm/addon-fit": "^0.11.0", @@ -34,8 +34,8 @@ "standard": "^17.1.2", "unplugin-vue-components": "^31.0.0", "vite": "^7.3.1", - "vue": "^3.5.28", + "vue": "^3.5.29", "vue-i18n": "^11.2.8", - "vue-router": "^5.0.2" + "vue-router": "^5.0.3" } }
frontend/package-lock.json+84 −103 modified@@ -11,7 +11,7 @@ "dependencies": { "@connectrpc/connect": "^2.1.1", "@connectrpc/connect-web": "^2.1.1", - "@hugeicons/core-free-icons": "^3.1.1", + "@hugeicons/core-free-icons": "^3.3.0", "@hugeicons/vue": "^1.0.4", "@vitejs/plugin-vue": "^6.0.4", "@xterm/addon-fit": "^0.11.0", @@ -21,13 +21,13 @@ "standard": "^17.1.2", "unplugin-vue-components": "^31.0.0", "vite": "^7.3.1", - "vue": "^3.5.28", + "vue": "^3.5.29", "vue-i18n": "^11.2.8", - "vue-router": "^5.0.2" + "vue-router": "^5.0.3" }, "devDependencies": { "process": "^0.11.10", - "stylelint": "^17.3.0", + "stylelint": "^17.4.0", "stylelint-config-standard": "^40.0.0" } }, @@ -905,9 +905,9 @@ } }, "node_modules/@hugeicons/core-free-icons": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-3.1.1.tgz", - "integrity": "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-3.3.0.tgz", + "integrity": "sha512-qYyr4JQ2eQIHTSTbITvnJvs6ERNK64D9gpwZnf2IyuG0exzqfyABLO/oTB71FB3RZPfu1GbwycdiGSo46apjMQ==", "license": "MIT" }, "node_modules/@hugeicons/vue": { @@ -1435,53 +1435,53 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", - "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.28", + "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", - "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", - "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.28", - "@vue/compiler-dom": "3.5.28", - "@vue/compiler-ssr": "3.5.28", - "@vue/shared": "3.5.28", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", - "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/devtools-api": { @@ -1491,12 +1491,12 @@ "license": "MIT" }, "node_modules/@vue/devtools-kit": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz", - "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.6.tgz", + "integrity": "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.0.5", + "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -1506,62 +1506,62 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz", - "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.6.tgz", + "integrity": "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==", "license": "MIT", "dependencies": { "rfdc": "^1.4.1" } }, "node_modules/@vue/reactivity": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", - "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.28" + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", - "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", - "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.28", - "@vue/runtime-core": "3.5.28", - "@vue/shared": "3.5.28", + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", - "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { - "vue": "3.5.28" + "vue": "3.5.29" } }, "node_modules/@vue/shared": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", - "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "license": "MIT" }, "node_modules/@xterm/addon-fit": { @@ -2121,13 +2121,13 @@ } }, "node_modules/css-functions-list": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", - "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12 || >=16" + "node": ">=12" } }, "node_modules/css-tree": { @@ -4276,13 +4276,6 @@ "node": ">=0.10.0" } }, - "node_modules/known-css-properties": { - "version": "0.37.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", - "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", - "dev": true, - "license": "MIT" - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5854,9 +5847,9 @@ } }, "node_modules/stylelint": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.3.0.tgz", - "integrity": "sha512-1POV91lcEMhj6SLVaOeA0KlS9yattS+qq+cyWqP/nYzWco7K5jznpGH1ExngvPlTM9QF1Kjd2bmuzJu9TH2OcA==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.4.0.tgz", + "integrity": "sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==", "dev": true, "funding": [ { @@ -5872,15 +5865,14 @@ "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.26", + "@csstools/css-syntax-patches-for-csstree": "^1.0.27", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", - "balanced-match": "^3.0.1", "colord": "^2.9.3", "cosmiconfig": "^9.0.0", - "css-functions-list": "^3.2.3", + "css-functions-list": "^3.3.3", "css-tree": "^3.1.0", "debug": "^4.4.3", "fast-glob": "^3.3.3", @@ -5894,7 +5886,6 @@ "import-meta-resolve": "^4.2.0", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", - "known-css-properties": "^0.37.0", "mathml-tag-names": "^4.0.0", "meow": "^14.0.0", "micromatch": "^4.0.8", @@ -5979,16 +5970,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/stylelint/node_modules/balanced-match": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", - "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/stylelint/node_modules/file-entry-cache": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.2.tgz", @@ -6615,16 +6596,16 @@ } }, "node_modules/vue": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", - "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.28", - "@vue/compiler-sfc": "3.5.28", - "@vue/runtime-dom": "3.5.28", - "@vue/server-renderer": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" @@ -6656,14 +6637,14 @@ } }, "node_modules/vue-router": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz", - "integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz", + "integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==", "license": "MIT", "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", - "@vue/devtools-api": "^8.0.0", + "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", @@ -6701,12 +6682,12 @@ } }, "node_modules/vue-router/node_modules/@vue/devtools-api": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.5.tgz", - "integrity": "sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.0.6.tgz", + "integrity": "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.0.5" + "@vue/devtools-kit": "^8.0.6" } }, "node_modules/vue-router/node_modules/json5": {
integration-tests/package.json+2 −2 modified@@ -12,9 +12,9 @@ "license": "AGPL-3.0-only", "devDependencies": { "chai": "^6.2.2", - "eslint": "^9.39.2", + "eslint": "^10.0.2", "mocha": "^11.7.5", - "selenium-webdriver": "^4.40.0" + "selenium-webdriver": "^4.41.0" }, "dependencies": { "wait-on": "^9.0.4"
integration-tests/package-lock.json+114 −205 modified@@ -13,9 +13,9 @@ }, "devDependencies": { "chai": "^6.2.2", - "eslint": "^9.39.2", + "eslint": "^10.0.2", "mocha": "^11.7.5", - "selenium-webdriver": "^4.40.0" + "selenium-webdriver": "^4.41.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -67,115 +67,78 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.2", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.1.0", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@hapi/address": { @@ -326,10 +289,17 @@ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -341,9 +311,9 @@ "license": "MIT" }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -364,9 +334,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -438,14 +408,26 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/browser-stdout": { @@ -467,16 +449,6 @@ "node": ">= 0.4" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -639,13 +611,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -818,33 +783,30 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", + "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.2", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -854,16 +816,15 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -878,58 +839,61 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -955,6 +919,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1216,19 +1181,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1314,23 +1266,6 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "dev": true }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1552,12 +1487,6 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -1612,16 +1541,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -1793,19 +1725,6 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1928,16 +1847,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -1954,9 +1863,9 @@ "dev": true }, "node_modules/selenium-webdriver": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.40.0.tgz", - "integrity": "sha512-dU0QbnVKdPmoNP8OtMCazRdtU2Ux6Wl4FEpG1iwUbDeajJK1dBAywBLrC1D7YFRtogHzN96AbXBgBAJaarcysw==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.41.0.tgz", + "integrity": "sha512-1XxuKVhr9az24xwixPBEDGSZP+P0z3ZOnCmr9Oiep0MlJN2Mk+flIjD3iBS9BgyjS4g14dikMqnrYUPIjhQBhA==", "dev": true, "funding": [ { @@ -1973,7 +1882,7 @@ "@bazel/runfiles": "^6.5.0", "jszip": "^3.10.1", "tmp": "^0.2.5", - "ws": "^8.18.3" + "ws": "^8.19.0" }, "engines": { "node": ">= 20.0.0" @@ -2347,9 +2256,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, "license": "MIT", "engines": {
.pre-commit-config.yaml+29 −2 modified@@ -9,6 +9,19 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files + - id: check-merge-conflict + - id: detect-private-key + - id: mixed-line-ending + args: ['--fix', 'lf'] + - id: check-json + exclude: | + (?x)^( + service/internal/entities/testdata/.*\.json| + integration-tests/tests/.*/entities/.*\.json| + var/entities/.*\.json + )$ + - id: check-case-conflict + - id: detect-aws-credentials # Alternative semantic commit checker - repo: https://github.com/compilerla/conventional-pre-commit @@ -34,9 +47,23 @@ repos: pass_filenames: false always_run: true + - id: service-unittests + name: service-unittests + entry: make service-unittests + language: system + pass_filenames: false + always_run: true + + - id: service-build + name: service-build + entry: make service + language: system + pass_filenames: false + always_run: true + - id: it - name: it - entry: make service-codestyle frontend-codestyle + name: integration-tests + entry: make it language: system pass_filenames: false always_run: true
service/internal/api/api.go+68 −49 modified@@ -3,6 +3,7 @@ package api import ( ctx "context" "encoding/json" + "errors" "os" "path" "sort" @@ -144,6 +145,9 @@ func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1 hash, err := createHash(req.Msg.Password) if err != nil { + if errors.Is(err, ErrArgon2Busy) { + return nil, connect.NewError(connect.CodeResourceExhausted, err) + } return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("error creating hash: %w", err)) } @@ -154,52 +158,50 @@ func (api *oliveTinAPI) PasswordHash(ctx ctx.Context, req *connect.Request[apiv1 return connect.NewResponse(ret), nil } -func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) { - // Check if local user authentication is enabled - if !api.cfg.AuthLocalUsers.Enabled { - return connect.NewResponse(&apiv1.LocalUserLoginResponse{ - Success: false, - }), nil - } - - match := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password) - - response := connect.NewResponse(&apiv1.LocalUserLoginResponse{ - Success: match, - }) +func (api *oliveTinAPI) cookieSecure(header http.Header) bool { + useTLS := header.Get("X-Forwarded-Proto") == "https" + return useTLS || api.cfg.Security.ForceSecureCookies +} +func (api *oliveTinAPI) applyLocalLoginResult(req *apiv1.LocalUserLoginRequest, response *connect.Response[apiv1.LocalUserLoginResponse], match bool, secure bool) { if match { - // Set authentication cookie for successful login - user := api.cfg.FindUserByUsername(req.Msg.Username) + user := api.cfg.FindUserByUsername(req.Username) if user != nil { sid := uuid.NewString() - // Register the session in the session storage auth.RegisterUserSession(api.cfg, "local", sid, user.Username) - - log.WithFields(log.Fields{ - "username": user.Username, - }).Info("LocalUserLogin: Session created and registered") - - // Set the authentication cookie in the response headers + log.WithFields(log.Fields{"username": user.Username}).Info("LocalUserLogin: Session created and registered") cookie := &http.Cookie{ Name: "olivetin-sid-local", Value: sid, - MaxAge: 31556952, // 1 year + MaxAge: 31556952, HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Set("Set-Cookie", cookie.String()) + log.WithFields(log.Fields{"username": user.Username}).Info("LocalUserLogin: User logged in successfully.") + } else { + log.WithFields(log.Fields{"username": req.Username}).Warn("LocalUserLogin: Password matched but user lookup failed.") } - - log.WithFields(log.Fields{ - "username": req.Msg.Username, - }).Info("LocalUserLogin: User logged in successfully.") } else { - log.WithFields(log.Fields{ - "username": req.Msg.Username, - }).Warn("LocalUserLogin: User login failed.") + log.WithFields(log.Fields{"username": req.Username}).Warn("LocalUserLogin: User login failed.") } +} +func (api *oliveTinAPI) LocalUserLogin(ctx ctx.Context, req *connect.Request[apiv1.LocalUserLoginRequest]) (*connect.Response[apiv1.LocalUserLoginResponse], error) { + if !api.cfg.AuthLocalUsers.Enabled { + return connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: false}), nil + } + match, err := checkUserPassword(api.cfg, req.Msg.Username, req.Msg.Password) + if err != nil { + if errors.Is(err, ErrArgon2Busy) { + return nil, connect.NewError(connect.CodeResourceExhausted, err) + } + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("checking password: %w", err)) + } + response := connect.NewResponse(&apiv1.LocalUserLoginResponse{Success: match}) + api.applyLocalLoginResult(req.Msg, response, match, api.cookieSecure(req.Header())) return response, nil } @@ -354,30 +356,36 @@ func getMostRecentExecutionStatusByActionId(api *oliveTinAPI, actionId string) * return ile } -func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) { - res := &apiv1.ExecutionStatusResponse{} +func (api *oliveTinAPI) resolveExecutionStatusForView(msg *apiv1.ExecutionStatusRequest, user *authpublic.AuthenticatedUser) (*executor.InternalLogEntry, error) { + ile := api.getExecutionStatusByRequest(msg) + if ile == nil { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", msg.ExecutionTrackingId, msg.ActionId)) + } + if !isValidLogEntry(ile) || !api.isLogEntryAllowed(ile, user) { + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("permission denied to view this execution")) + } + return ile, nil +} - user := auth.UserFromApiCall(ctx, req, api.cfg) +func (api *oliveTinAPI) getExecutionStatusByRequest(msg *apiv1.ExecutionStatusRequest) *executor.InternalLogEntry { + if msg.ExecutionTrackingId != "" { + return getExecutionStatusByTrackingID(api, msg.ExecutionTrackingId) + } + return getMostRecentExecutionStatusByActionId(api, msg.ActionId) +} +func (api *oliveTinAPI) ExecutionStatus(ctx ctx.Context, req *connect.Request[apiv1.ExecutionStatusRequest]) (*connect.Response[apiv1.ExecutionStatusResponse], error) { + user := auth.UserFromApiCall(ctx, req, api.cfg) if err := api.checkDashboardAccess(user); err != nil { return nil, err } - - var ile *executor.InternalLogEntry - - if req.Msg.ExecutionTrackingId != "" { - ile = getExecutionStatusByTrackingID(api, req.Msg.ExecutionTrackingId) - - } else { - ile = getMostRecentExecutionStatusByActionId(api, req.Msg.ActionId) + ile, err := api.resolveExecutionStatusForView(req.Msg, user) + if err != nil { + return nil, err } - - if ile == nil { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found for tracking ID %s or action ID %s", req.Msg.ExecutionTrackingId, req.Msg.ActionId)) - } else { - res.LogEntry = api.internalLogEntryToPb(ile, user) + res := &apiv1.ExecutionStatusResponse{ + LogEntry: api.internalLogEntryToPb(ile, user), } - return connect.NewResponse(res), nil } @@ -390,6 +398,7 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou }).Info("Logout: User logged out") response := connect.NewResponse(&apiv1.LogoutResponse{}) + secure := api.cookieSecure(req.Header()) // Clear the local authentication cookie by setting it to expire localCookie := &http.Cookie{ @@ -398,6 +407,8 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou MaxAge: -1, // This tells the browser to delete the cookie HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Set("Set-Cookie", localCookie.String()) @@ -408,6 +419,8 @@ func (api *oliveTinAPI) Logout(ctx ctx.Context, req *connect.Request[apiv1.Logou MaxAge: -1, // This tells the browser to delete the cookie HttpOnly: true, Path: "/", + Secure: secure, + SameSite: http.SameSiteLaxMode, } response.Header().Add("Set-Cookie", oauth2Cookie.String()) @@ -882,11 +895,17 @@ func (api *oliveTinAPI) OnExecutionFinished(ile *executor.InternalLogEntry) { } func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) { + user := auth.UserFromApiCall(ctx, req, api.cfg) + if err := api.checkDashboardAccess(user); err != nil { + return nil, err + } + if !user.EffectivePolicy.ShowDiagnostics { + return nil, connect.NewError(connect.CodePermissionDenied, fmt.Errorf("diagnostics are not available for your account")) + } res := &apiv1.GetDiagnosticsResponse{ SshFoundKey: installationinfo.Runtime.SshFoundKey, SshFoundConfig: installationinfo.Runtime.SshFoundConfig, } - return connect.NewResponse(res), nil } @@ -1263,7 +1282,7 @@ func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv return api.StartAction(ctx, &connect.Request[apiv1.StartActionRequest]{ Msg: &apiv1.StartActionRequest{ - // FIXME + BindingId: execReqLogEntry.GetBindingId(), UniqueTrackingId: req.Msg.ExecutionTrackingId, }, })
service/internal/api/local_user_login.go+40 −24 modified@@ -1,13 +1,20 @@ package api import ( + "errors" "runtime" config "github.com/OliveTin/OliveTin/internal/config" "github.com/alexedwards/argon2id" log "github.com/sirupsen/logrus" ) +var ErrArgon2Busy = errors.New("too many concurrent password operations") + +const argon2MaxConcurrent = 10 + +var argon2Sem = make(chan struct{}, argon2MaxConcurrent) + var defaultParams = argon2id.Params{ Memory: 64 * 1024, Iterations: 4, @@ -17,10 +24,16 @@ var defaultParams = argon2id.Params{ } func CreateHash(password string) (string, error) { + select { + case argon2Sem <- struct{}{}: + defer func() { <-argon2Sem }() + default: + return "", ErrArgon2Busy + } hash, err := argon2id.CreateHash(password, &defaultParams) if err != nil { - log.Fatal("Error creating hash: ", err) + log.Warnf("Error creating hash: %v", err) return "", err } @@ -31,37 +44,40 @@ func createHash(password string) (string, error) { return CreateHash(password) } -func comparePasswordAndHash(password, hash string) bool { +func comparePasswordAndHash(password, hash string) (bool, error) { + select { + case argon2Sem <- struct{}{}: + defer func() { <-argon2Sem }() + default: + return false, ErrArgon2Busy + } match, err := argon2id.ComparePasswordAndHash(password, hash) if err != nil { log.Errorf("Error comparing password and hash: %v", err) - return false + return false, nil } - return match + return match, nil } -func checkUserPassword(cfg *config.Config, username, password string) bool { - for _, user := range cfg.AuthLocalUsers.Users { - if user.Username == username { - match := comparePasswordAndHash(password, user.Password) - - if match { - return true - } else { - log.WithFields(log.Fields{ - "username": username, - }).Warn("Password does not match for user") - - return false - } - } +func checkUserPassword(cfg *config.Config, username, password string) (bool, error) { + user := cfg.FindUserByUsername(username) + if user == nil { + log.WithFields(log.Fields{"username": username}).Warn("Failed to check password for user, as username was not found") + return false, nil } + return comparePasswordAndLogResult(password, user.Password, username) +} - log.WithFields(log.Fields{ - "username": username, - }).Warn("Failed to check password for user, as username was not found") - - return false +func comparePasswordAndLogResult(password, hash, username string) (bool, error) { + match, err := comparePasswordAndHash(password, hash) + if err != nil { + return false, err + } + if !match { + log.WithFields(log.Fields{"username": username}).Warn("Password does not match for user") + return false, nil + } + return true, nil }
service/internal/auth/otoauth2/restapi_auth_oauth2.go+7 −1 modified@@ -108,14 +108,20 @@ func randString(nByte int) (string, error) { return base64.URLEncoding.EncodeToString(b), nil } +func (h *OAuth2Handler) cookieSecure(r *http.Request) bool { + useTLS := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" + return useTLS || h.cfg.Security.ForceSecureCookies +} + func (h *OAuth2Handler) setOAuthCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) { cookie := &http.Cookie{ Name: name, Value: value, MaxAge: 900, // 15 minutes - Secure: r.TLS != nil, + Secure: h.cookieSecure(r), HttpOnly: true, Path: "/", + SameSite: http.SameSiteLaxMode, } http.SetCookie(w, cookie)
service/internal/config/config.go+16 −0 modified@@ -107,6 +107,16 @@ type PrometheusConfig struct { DefaultGoMetrics bool `koanf:"defaultGoMetrics"` } +// SecurityConfig allows users to fine tune the security related HTTP headers and cookie options. +type SecurityConfig struct { + HeaderContentSecurityPolicy bool `koanf:"headerContentSecurityPolicy"` + ContentSecurityPolicy string `koanf:"contentSecurityPolicy"` + HeaderXContentTypeOptions bool `koanf:"headerXContentTypeOptions"` + HeaderXFrameOptions bool `koanf:"headerXFrameOptions"` + XFrameOptions string `koanf:"xFrameOptions"` + ForceSecureCookies bool `koanf:"forceSecureCookies"` +} + // Config is the global config used through the whole app. type Config struct { UseSingleHTTPFrontend bool `koanf:"useSingleHTTPFrontend"` @@ -160,6 +170,7 @@ type Config struct { InsecureAllowDumpActionMap bool `koanf:"insecureAllowDumpActionMap"` InsecureAllowDumpJwtClaims bool `koanf:"insecureAllowDumpJwtClaims"` Prometheus PrometheusConfig `koanf:"prometheus"` + Security SecurityConfig `koanf:"security"` SaveLogs SaveLogsConfig `koanf:"saveLogs"` DefaultIconForActions string `koanf:"defaultIconForActions"` DefaultIconForDirectories string `koanf:"defaultIconForDirectories"` @@ -268,6 +279,11 @@ func DefaultConfigWithBasePort(basePort int) *Config { config.InsecureAllowDumpJwtClaims = false config.Prometheus.Enabled = false config.Prometheus.DefaultGoMetrics = false + config.Security.HeaderContentSecurityPolicy = true + config.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'" + config.Security.HeaderXContentTypeOptions = true + config.Security.HeaderXFrameOptions = true + config.Security.XFrameOptions = "DENY" config.DefaultIconForActions = "😀" config.DefaultIconForDirectories = "📁" config.DefaultIconForBack = "«"
service/internal/config/sanitize.go+21 −2 modified@@ -16,6 +16,7 @@ func (cfg *Config) Sanitize() { cfg.sanitizeAuthRequireGuestsToLogin() cfg.sanitizeLogHistoryPageSize() cfg.sanitizeLocalUserPasswords() + cfg.sanitizeSecurityHeaders() // log.Infof("cfg %p", cfg) @@ -183,6 +184,25 @@ func (cfg *Config) sanitizeLocalUserPasswords() { } } +func (cfg *Config) sanitizeSecurityHeaders() { + cfg.sanitizeSecurityHeadersCSP() + cfg.sanitizeSecurityHeadersXFrameOptions() +} + +func (cfg *Config) sanitizeSecurityHeadersCSP() { + if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy != "" { + return + } + cfg.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self'" +} + +func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() { + if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions != "" { + return + } + cfg.Security.XFrameOptions = "DENY" +} + // parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment. func parsePasswordTemplate(source string) string { t, err := template.New("password").Option("missingkey=error").Parse(source) @@ -239,8 +259,7 @@ func (arg *ActionArgument) sanitize() { arg.sanitizeNoType() - // TODO Validate the default against the type checker, but this creates a - // import loop + // Default value validation runs in executor at config load (validateArgumentDefaults). } func (arg *ActionArgument) sanitizeNoType() {
service/internal/cors/cors.go+0 −23 removed@@ -1,23 +0,0 @@ -package cors - -import ( - log "github.com/sirupsen/logrus" - "net/http" -) - -// AllowCors takes a HTTP handler and adds Access-Control-Allow-Origin headers to -// responses. -// -// Note: HTTP OPTIONS requests (which need to be preflighted" for CORS) are not -// handled because this app does not use HTTP PUT/PATCH/etc. -func AllowCors(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if origin := r.Header.Get("Origin"); origin != "" { - log.Debugf("Adding CORS header origin: %q", origin) - - w.Header().Set("Access-Control-Allow-Origin", origin) - } - - h.ServeHTTP(w, r) - }) -}
service/internal/cors/cors_test.go+0 −22 removed@@ -1,22 +0,0 @@ -package cors - -import ( - "github.com/stretchr/testify/assert" - "net/http" - "net/http/httptest" - "testing" -) - -func TestCors(t *testing.T) { - req, _ := http.NewRequest("GET", "/health-check", nil) - req.Header.Add("Origin", "1.2.3.4") - - blat := AllowCors(http.FileServer(http.Dir("."))) - - rr := httptest.NewRecorder() - - blat.ServeHTTP(rr, req) - - assert.Equal(t, http.StatusNotFound, rr.Code, "HTTP 404 on CORS") - assert.Equal(t, "1.2.3.4", rr.Header().Get("Access-Control-Allow-Origin"), "CORS Header set") -}
service/internal/executor/executor_actions.go+34 −0 modified@@ -41,7 +41,41 @@ type RebuildActionMapRequest struct { DashboardActionTitles []string } +func validateArgumentDefaults(cfg *config.Config) { + if cfg == nil { + return + } + for _, action := range cfg.Actions { + validateActionArgumentDefaults(action) + } +} + +func validateActionArgumentDefaults(action *config.Action) { + if action == nil { + return + } + for i := range action.Arguments { + validateArgumentDefault(action, &action.Arguments[i]) + } +} + +func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) { + if arg.Default == "" { + return + } + if err := ValidateArgument(arg, arg.Default, action); err != nil { + log.WithFields(log.Fields{ + "actionTitle": action.Title, + "argName": arg.Name, + "default": arg.Default, + "error": err, + }).Warn("Argument default value failed validation") + } +} + func (e *Executor) RebuildActionMap() { + validateArgumentDefaults(e.Cfg) + e.MapActionBindingsLock.Lock() clear(e.MapActionBindings)
service/internal/executor/executor.go+34 −21 modified@@ -603,14 +603,14 @@ func getExecutionsCount(rate config.RateSpec, req *ExecutionRequest) int { then := time.Now().Add(-duration) + currentEntityPrefix := "" + if req.Binding != nil && req.Binding.Entity != nil { + currentEntityPrefix = req.Binding.Entity.UniqueKey + } for _, logEntry := range req.executor.GetLogsByBindingId(req.Binding.ID) { - // FIXME - /* - if logEntry.EntityPrefix != req.EntityPrefix { - continue - } - */ - + if logEntry.EntityPrefix != currentEntityPrefix { + continue + } if logEntry.DatetimeStarted.After(then) && !logEntry.Blocked { executions += 1 @@ -761,37 +761,50 @@ func fail(req *ExecutionRequest, err error) bool { func stepRequestAction(req *ExecutionRequest) bool { metricActionsRequested.Inc() - // If there is no binding or action, do not proceed. Leave default - // log entry values (icon/title/id) and stop execution gracefully. + if !stepRequestActionHasBinding(req) { + return false + } + + stepRequestActionPopulateLogEntry(req) + stepRequestActionRegisterLog(req) + + log.WithFields(log.Fields{ + "actionTitle": req.logEntry.ActionTitle, + "tags": req.Tags, + }).Infof("Action requested") + + notifyListenersStarted(req) + + return true +} + +func stepRequestActionHasBinding(req *ExecutionRequest) bool { if req.Binding == nil || req.Binding.Action == nil { log.Warnf("Action request has no binding/action; skipping execution") return false } + return true +} +func stepRequestActionPopulateLogEntry(req *ExecutionRequest) { req.logEntry.Binding = req.Binding req.logEntry.ActionConfigTitle = req.Binding.Action.Title req.logEntry.ActionTitle = tpl.ParseTemplateOfActionBeforeExec(req.Binding.Action.Title, req.Binding.Entity) req.logEntry.ActionIcon = req.Binding.Action.Icon req.logEntry.Tags = req.Tags + if req.Binding.Entity != nil { + req.logEntry.EntityPrefix = req.Binding.Entity.UniqueKey + } +} +func stepRequestActionRegisterLog(req *ExecutionRequest) { req.executor.logmutex.Lock() + defer req.executor.logmutex.Unlock() if _, containsKey := req.executor.LogsByBindingId[req.Binding.ID]; !containsKey { req.executor.LogsByBindingId[req.Binding.ID] = make([]*InternalLogEntry, 0) } - req.executor.LogsByBindingId[req.Binding.ID] = append(req.executor.LogsByBindingId[req.Binding.ID], req.logEntry) - - req.executor.logmutex.Unlock() - - log.WithFields(log.Fields{ - "actionTitle": req.logEntry.ActionTitle, - "tags": req.Tags, - }).Infof("Action requested") - - notifyListenersStarted(req) - - return true } func stepLogStart(req *ExecutionRequest) bool {
service/internal/httpservers/frontend.go+35 −1 modified@@ -23,6 +23,40 @@ import ( log "github.com/sirupsen/logrus" ) +func applySecurityHeaders(cfg *config.Config, w http.ResponseWriter) { + applyCSP(cfg, w) + applyXContentTypeOptions(cfg, w) + applyXFrameOptions(cfg, w) +} + +func applyCSP(cfg *config.Config, w http.ResponseWriter) { + if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy == "" { + return + } + w.Header().Set("Content-Security-Policy", cfg.Security.ContentSecurityPolicy) +} + +func applyXContentTypeOptions(cfg *config.Config, w http.ResponseWriter) { + if !cfg.Security.HeaderXContentTypeOptions { + return + } + w.Header().Set("X-Content-Type-Options", "nosniff") +} + +func applyXFrameOptions(cfg *config.Config, w http.ResponseWriter) { + if !cfg.Security.HeaderXFrameOptions || cfg.Security.XFrameOptions == "" { + return + } + w.Header().Set("X-Frame-Options", cfg.Security.XFrameOptions) +} + +func securityHeadersMiddleware(cfg *config.Config, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + applySecurityHeaders(cfg, w) + next.ServeHTTP(w, r) + }) +} + func logDebugRequest(cfg *config.Config, source string, r *http.Request) { if cfg.LogDebugOptions.SingleFrontendRequests { log.Debugf("SingleFrontend HTTP Req URL %v: %q", source, r.URL) @@ -96,7 +130,7 @@ func StartFrontendMux(cfg *config.Config, ex *executor.Executor) { srv := &http.Server{ Addr: cfg.ListenAddressSingleHTTPFrontend, - Handler: mux, + Handler: securityHeadersMiddleware(cfg, mux), } log.Fatal(srv.ListenAndServe())
service/internal/httpservers/webuiServer.go+0 −2 modified@@ -1,8 +1,6 @@ package httpservers import ( - - // cors "github.com/OliveTin/OliveTin/internal/cors" "net/http" "os" "path"
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-pc8g-78pf-4xrpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-28342ghsaADVISORY
- github.com/OliveTin/OliveTin/commit/2eb5f0ba79d4bbef3c802bf8b4666a7e18dcfd90ghsax_refsource_MISCWEB
- github.com/OliveTin/OliveTin/releases/tag/3000.10.2ghsax_refsource_MISCWEB
- github.com/OliveTin/OliveTin/security/advisories/GHSA-pc8g-78pf-4xrpghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.