From 3274778e405d0ded181e6e9759f77ae8202e1683 Mon Sep 17 00:00:00 2001 From: Franz Rolfsvaag Date: Sat, 18 Jul 2026 18:37:42 +0200 Subject: [PATCH] Fix clobbered form endpoints --- CHANGELOG.md | 5 ++++ TODO.md | 1 + docs/lumi-ai-improvement-center.md | 2 ++ knowledge/core/lumi-core.md | 2 +- knowledge/plugins/lumi-ai.md | 2 +- package-lock.json | 4 +-- package.json | 2 +- plugins/lumi_ai/CHANGELOG.md | 5 ++++ plugins/lumi_ai/plugin.json | 6 ++-- plugins/lumi_ai/public/improvement-center.js | 5 ++-- plugins/lumi_ai/public/settings.js | 2 +- plugins/lumi_ai/views/improvement-center.ejs | 6 ++-- release-index.json | 30 ++++++++++++++++++++ scripts/verify-destructive-actions.js | 4 ++- scripts/verify-release-metadata.js | 8 +++--- scripts/verify-update-system.js | 5 ++-- scripts/verify-webui.js | 5 ++-- src/web/public/app.js | 15 ++++++---- src/web/public/lumi-interactions.js | 7 +++-- update-manifest.json | 16 +++++++++-- 20 files changed, 99 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c85bd84..e6bd1fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Lumi changelog +## 0.2.9 + +- Fixed forms containing fields named `action` or `method` being able to shadow their own submission endpoint and send requests to an invalid URL. +- Added a shared clobber-safe form action/method resolver and reused it in update, settings, and Lumi AI feedback workflows. + ## 0.2.8 - Fixed Lumi AI Improvement Center actions that could return an unhelpful generic failure for no-change, dismiss, edit, and restore operations. diff --git a/TODO.md b/TODO.md index 338d417..5b56225 100644 --- a/TODO.md +++ b/TODO.md @@ -677,6 +677,7 @@ This section is for Lumi chat answer feedback and AI Improvement Center work, no ## Done +- 2026-07-18: Fixed core 0.2.9 and Lumi AI 0.8.5 forms whose `action` or `method` fields could shadow the form endpoint, added a shared clobber-safe resolver, and migrated the affected feedback/settings requests to it. - 2026-07-18: Fixed Lumi AI 0.8.4 Improvement Center state changes for no-change, dismiss, edit, and restore actions with an explicit authenticated JSON contract, actionable errors, safe diagnostics, and focused regression coverage. - 2026-07-18: Fixed private reverse-proxy HTTPS recognition for production diagnostics in core 0.2.5 without globally trusting client forwarding headers, and taught the local client to consume the repository's env-style `.secrets` file and short diagnostic variable names directly. - 2026-07-18: Fixed Windows/network-share plugin updates in core 0.2.6 by leaving live preserved plugin data in place and transactionally replacing only code, including automatic code rollback coverage. diff --git a/docs/lumi-ai-improvement-center.md b/docs/lumi-ai-improvement-center.md index 4595a7e..c836fac 100644 --- a/docs/lumi-ai-improvement-center.md +++ b/docs/lumi-ai-improvement-center.md @@ -31,6 +31,8 @@ Review actions use an authenticated JSON response even when Lumi is behind a reverse proxy. If a save cannot complete, the page distinguishes an expired session, insufficient access, a feedback record changed elsewhere, and a server-side storage problem instead of showing a generic failure. +Lumi resolves the authored form endpoint directly, so fields named `action` +cannot replace the destination URL in the browser. ## Advanced tools diff --git a/knowledge/core/lumi-core.md b/knowledge/core/lumi-core.md index 8808cc0..595c35c 100644 --- a/knowledge/core/lumi-core.md +++ b/knowledge/core/lumi-core.md @@ -14,7 +14,7 @@ editable: false Lumi is the core web UI and bot runtime. ## Runtime Package: lumi-bot -Version: 0.2.8 +Version: 0.2.9 ## Routes - GET /api/events - POST /api/destructive-confirmations diff --git a/knowledge/plugins/lumi-ai.md b/knowledge/plugins/lumi-ai.md index 6735dca..92123b3 100644 --- a/knowledge/plugins/lumi-ai.md +++ b/knowledge/plugins/lumi-ai.md @@ -14,7 +14,7 @@ editable: false Managed local AI provider and scoped WebUI assistant for Lumi. ## Metadata Plugin ID: lumi_ai -Version: 0.8.4 +Version: 0.8.5 Default state: enabled ## Web Routes - /plugins/lumi_ai diff --git a/package-lock.json b/package-lock.json index e4eef08..7f5800e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lumi-bot", - "version": "0.2.8", + "version": "0.2.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumi-bot", - "version": "0.2.8", + "version": "0.2.9", "dependencies": { "adm-zip": "^0.5.12", "better-sqlite3": "^11.5.0", diff --git a/package.json b/package.json index 99f4c32..9b1155a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumi-bot", - "version": "0.2.8", + "version": "0.2.9", "private": true, "type": "commonjs", "scripts": { diff --git a/plugins/lumi_ai/CHANGELOG.md b/plugins/lumi_ai/CHANGELOG.md index f2a08e6..7d922b6 100644 --- a/plugins/lumi_ai/CHANGELOG.md +++ b/plugins/lumi_ai/CHANGELOG.md @@ -1,5 +1,10 @@ # Lumi AI changelog +## 0.8.5 + +- Fixed Improvement Center actions returning HTTP 404 because their hidden `action` field could shadow the form's endpoint URL in the browser. +- Reused Lumi's shared clobber-safe form resolver for feedback actions and asynchronous Lumi AI settings/download forms. + ## 0.8.4 - Fixed no-change, dismiss, edit, and restore actions in the Improvement Center by using an explicit authenticated JSON response contract. diff --git a/plugins/lumi_ai/plugin.json b/plugins/lumi_ai/plugin.json index ae07ccb..66d5d64 100644 --- a/plugins/lumi_ai/plugin.json +++ b/plugins/lumi_ai/plugin.json @@ -1,11 +1,11 @@ { "id": "lumi_ai", "name": "Lumi AI", - "version": "0.8.4", + "version": "0.8.5", "description": "Managed local AI provider and scoped WebUI assistant for Lumi.", "main": "index.js", "channel": "stable", - "compatible_from": "0.8.3", - "migration_notes": "Existing models, runtimes, settings, feedback, corrections, metrics, and tool data are retained. Feedback actions now use a reliable JSON response contract and report actionable failures; no manual migration is required.", + "compatible_from": "0.8.4", + "migration_notes": "Existing models, runtimes, settings, feedback, corrections, metrics, and tool data are retained. Feedback and settings forms now use Lumi's shared clobber-safe endpoint resolver; no manual migration is required.", "rollback_safe": true } diff --git a/plugins/lumi_ai/public/improvement-center.js b/plugins/lumi_ai/public/improvement-center.js index 698b89a..a2cb6e5 100644 --- a/plugins/lumi_ai/public/improvement-center.js +++ b/plugins/lumi_ai/public/improvement-center.js @@ -121,12 +121,13 @@ submitter.textContent = "Saving…"; } try { + const action = window.LumiForms?.action(form) || form.getAttribute("action") || window.location.href; const body = new URLSearchParams(); for (const [key, value] of new FormData(form).entries()) { if (typeof value === "string") body.append(key, value); } - const response = await fetch(form.action, { - method: form.method || "POST", + const response = await fetch(action, { + method: window.LumiForms?.method(form) || form.getAttribute("method") || "POST", headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", diff --git a/plugins/lumi_ai/public/settings.js b/plugins/lumi_ai/public/settings.js index a0983d0..03e0dca 100644 --- a/plugins/lumi_ai/public/settings.js +++ b/plugins/lumi_ai/public/settings.js @@ -50,7 +50,7 @@ const button = form.querySelector("[data-ai-download-button]"); window.LumiStateButton?.setState(button, "loading", { busy: true }); try { - const response = await fetch(form.action, { + const response = await fetch(window.LumiForms?.action(form) || form.getAttribute("action") || window.location.href, { method: "POST", headers: { "Accept": "application/json" }, body: new FormData(form) diff --git a/plugins/lumi_ai/views/improvement-center.ejs b/plugins/lumi_ai/views/improvement-center.ejs index 379a9e4..c0782ed 100644 --- a/plugins/lumi_ai/views/improvement-center.ejs +++ b/plugins/lumi_ai/views/improvement-center.ejs @@ -1,6 +1,6 @@ <%- include("../../../src/web/views/partials/layout-top", { title }) %> - - + + <% const statusLabel = (status) => ({ pending: "New", flagged: "Needs attention", verified: "Reviewed by moderator", approved: "Ready to apply", implemented: "Applied", rejected: "Dismissed", reviewed: "No change needed", archived: "Archived" }[status] || status); %>
@@ -188,5 +188,5 @@ <% } %> - + <%- include("../../../src/web/views/partials/layout-bottom") %> diff --git a/release-index.json b/release-index.json index 4f42fa3..977051b 100644 --- a/release-index.json +++ b/release-index.json @@ -2,6 +2,36 @@ "schema_version": 1, "channel": "stable", "releases": [ + { + "version": "0.2.9", + "ref": "refs/tags/v0.2.9", + "released_at": "2026-07-18", + "installable": true, + "rollback_safe": true, + "replaces_versions": [ + "1.2.0" + ], + "data_policy": "preserve", + "dependency_policy": "sync_on_restart", + "migration_notes": "Fixes browser form endpoint shadowing in Lumi AI feedback and shared settings workflows while preserving all existing feedback, settings, models, corrections, and plugin data.", + "plugins": { + "auto-vc": "0.1.6", + "birthday": "0.1.3", + "economy-framework": "0.2.10", + "economy-games": "0.1.7", + "expression-interaction": "0.2.1", + "lumi_ai": "0.8.5", + "moderation": "0.1.5", + "okf": "0.1.1", + "quotes": "0.1.2", + "sample-plugin": "0.1.0", + "throne_wishlist": "0.1.2", + "welcome_messages": "0.1.1" + }, + "tools": { + "lumi_ai_web_search": "0.1.1" + } + }, { "version": "0.2.8", "ref": "refs/tags/v0.2.8", diff --git a/scripts/verify-destructive-actions.js b/scripts/verify-destructive-actions.js index c853914..4071727 100644 --- a/scripts/verify-destructive-actions.js +++ b/scripts/verify-destructive-actions.js @@ -77,7 +77,8 @@ function verifySharedInfrastructure() { const appScript = fs.readFileSync(path.join(root, "src", "web", "public", "app.js"), "utf8"); assert(appScript.includes("submitter?.dataset?.confirmMode"), "Submit-button confirmation metadata is ignored"); assert(appScript.includes("submitter?.dataset?.confirmLabel || form.dataset.confirmLabel"), "The clicked submit button cannot override generic confirmation copy"); - assert(appScript.includes('submitter?.hasAttribute?.("formaction") ? submitter.formAction : form.action'), "Submit buttons without formaction incorrectly override their parent form action"); + assert(appScript.includes('form?.getAttribute?.("action")'), "Shared form actions do not resist controls named action shadowing the endpoint"); + assert(appScript.includes("window.LumiForms = Object.freeze"), "Plugins cannot reuse the shared clobber-safe form helper"); assert(appScript.includes('form.querySelector(\'input[name="confirmation_token"]\')'), "Timed form confirmation tokens are not attached to submitted forms"); assert(appScript.includes("const response = await fetch(action, requestOptions)"), "Async update requests do not submit through the shared confirmed form payload"); assert(!appScript.includes("window.LumiConfirm.destructiveFetch(form.action"), "Timed update forms request a second confirmation instead of reusing the form token"); @@ -97,6 +98,7 @@ function verifySharedInfrastructure() { assert(improvementView.includes("/corrections/<%= entry.id %>/delete")); const improvementScript = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "public", "improvement-center.js"), "utf8"); assert.equal(improvementScript.includes("window.confirm"), false, "Improvement Center still uses the legacy browser confirmation"); + assert(improvementScript.includes("window.LumiForms?.action(form)"), "Improvement Center bypasses the shared clobber-safe form endpoint helper"); } const controls = verifyViews(); diff --git a/scripts/verify-release-metadata.js b/scripts/verify-release-metadata.js index 9f8353a..6c2ea20 100644 --- a/scripts/verify-release-metadata.js +++ b/scripts/verify-release-metadata.js @@ -4,15 +4,15 @@ const path = require("path"); const { findSafeTarget } = require("../src/services/versioning"); const root = path.join(__dirname, ".."); -const releaseVersion = "0.2.8"; -const previousCoreVersion = "0.2.7"; +const releaseVersion = "0.2.9"; +const previousCoreVersion = "0.2.8"; const earliestCompatibleCoreVersion = "0.1.9"; const changedPlugins = { "auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" }, birthday: { from: "0.1.2", to: "0.1.3", knowledge: "birthday" }, "economy-framework": { from: "0.2.9", to: "0.2.10", knowledge: "economy-framework" }, "expression-interaction": { from: "0.2.0", to: "0.2.1", knowledge: "expression-interaction" }, - lumi_ai: { from: "0.8.3", to: "0.8.4", knowledge: "lumi-ai" }, + lumi_ai: { from: "0.8.4", to: "0.8.5", knowledge: "lumi-ai" }, moderation: { from: "0.1.4", to: "0.1.5", knowledge: "moderation" }, okf: { from: "0.1.0", to: "0.1.1", knowledge: "okf" }, quotes: { from: "0.1.1", to: "0.1.2", knowledge: "quotes" }, @@ -87,4 +87,4 @@ assert.equal(webSearch.minimum_lumi_version, "0.2.0"); assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2"); assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true); -console.log("Release metadata verification passed: core 0.2.8, Lumi AI 0.8.4, and synchronized package metadata."); +console.log("Release metadata verification passed: core 0.2.9, Lumi AI 0.8.5, and synchronized package metadata."); diff --git a/scripts/verify-update-system.js b/scripts/verify-update-system.js index 7e40ef6..919e852 100644 --- a/scripts/verify-update-system.js +++ b/scripts/verify-update-system.js @@ -16,7 +16,7 @@ function readJson(relativePath) { const releaseIndex = readJson("release-index.json"); const releaseVersions = releaseIndex.releases.map((release) => release.version); -assert.deepEqual(releaseVersions, ["0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]); +assert.deepEqual(releaseVersions, ["0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]); assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique"); for (const release of releaseIndex.releases) { assert.equal(normalizeRepositoryRef(release.ref), release.ref); @@ -37,6 +37,7 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) { const baseTarget = { current_version: "0.2.4", available_versions: [ + { version: "0.2.9", ref: "refs/tags/v0.2.9", rollback_safe: true }, { version: "0.2.8", ref: "refs/tags/v0.2.8", rollback_safe: true }, { version: "0.2.7", ref: "refs/tags/v0.2.7", rollback_safe: true }, { version: "0.2.6", ref: "refs/tags/v0.2.6", rollback_safe: true }, @@ -68,7 +69,7 @@ const corrected = buildStatus({ channel: "stable" }); assert.equal(corrected.version_correction, true); -assert.equal(corrected.safe_target_version, "0.2.8"); +assert.equal(corrected.safe_target_version, "0.2.9"); assert.equal(corrected.update_available, true); assert.equal(corrected.blocked, false); diff --git a/scripts/verify-webui.js b/scripts/verify-webui.js index ef3923b..a40ea5e 100644 --- a/scripts/verify-webui.js +++ b/scripts/verify-webui.js @@ -195,8 +195,9 @@ function verifySharedUpdateActions() { const dashboard = fs.readFileSync(path.join(root, "src", "web", "views", "admin-dashboard.ejs"), "utf8"); const settings = fs.readFileSync(path.join(root, "src", "web", "views", "admin-settings.ejs"), "utf8"); assert(appSource.includes('button[data-update-action]')); - assert(appSource.includes('submitter?.hasAttribute?.("formaction") ? submitter.formAction : form.action')); - assert(appSource.includes('submitter?.hasAttribute?.("formmethod") ? submitter.formMethod : form.method')); + assert(appSource.includes('form?.getAttribute?.("action")')); + assert(appSource.includes("window.LumiForms = Object.freeze")); + assert(appSource.includes('form?.getAttribute?.("method")')); assert(!appSource.includes("submitter?.formAction || form.action")); assert(appSource.includes('actionPath.endsWith("/check")')); assert(appSource.includes('form.matches("[data-update-check-form]")')); diff --git a/src/web/public/app.js b/src/web/public/app.js index 00a3d85..ada10fc 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1146,14 +1146,19 @@ let activeDestructive = null; let activeCallbackConfirm = null; - // A button without a formaction can report the current document URL through - // its formAction property. Only prefer the button value when the attribute - // was explicitly authored; otherwise the parent form owns the endpoint. + // Read authored attributes instead of form.action/form.method. Controls named + // "action" or "method" can shadow those DOM properties and turn the endpoint + // into an HTML element rather than a URL. const effectiveFormAction = (form, submitter = null) => - submitter?.hasAttribute?.("formaction") ? submitter.formAction : form.action; + submitter?.getAttribute?.("formaction") || form?.getAttribute?.("action") || window.location.href; const effectiveFormMethod = (form, submitter = null) => - submitter?.hasAttribute?.("formmethod") ? submitter.formMethod : form.method; + submitter?.getAttribute?.("formmethod") || form?.getAttribute?.("method") || "get"; + + window.LumiForms = Object.freeze({ + action: effectiveFormAction, + method: effectiveFormMethod + }); const destructiveAction = (form, submitter = null) => { try { diff --git a/src/web/public/lumi-interactions.js b/src/web/public/lumi-interactions.js index 272f477..860e304 100644 --- a/src/web/public/lumi-interactions.js +++ b/src/web/public/lumi-interactions.js @@ -114,12 +114,13 @@ status.textContent = "Saving..."; try { for (const form of forms) { + const action = window.LumiForms?.action(form) || form.getAttribute("action") || window.location.href; const body = new URLSearchParams(); for (const [key, value] of new FormData(form).entries()) { if (typeof value === "string") body.append(key, value); } - const response = await fetch(form.action || window.location.href, { - method: form.method || "POST", + const response = await fetch(action, { + method: window.LumiForms?.method(form) || form.getAttribute("method") || "POST", body, headers: { Accept: "text/html,application/json", @@ -127,7 +128,7 @@ }, redirect: "follow" }); - if (!response.ok) throw new Error(`Save failed for ${form.action || "settings form"}.`); + if (!response.ok) throw new Error(`Save failed for ${action || "settings form"}.`); form._lumiSnapshot = snapshotForm(form); } status.textContent = "Saved. Reloading..."; diff --git a/update-manifest.json b/update-manifest.json index c38e137..e70181e 100644 --- a/update-manifest.json +++ b/update-manifest.json @@ -1,6 +1,6 @@ { "name": "Lumi Core", - "version": "0.2.8", + "version": "0.2.9", "channel": "stable", "released_at": "2026-07-18", "compatible_from": "0.1.9", @@ -8,7 +8,7 @@ "replaces_versions": [ "1.2.0" ], - "migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics, Windows/network-share-safe plugin code replacement, and reliable Lumi AI feedback review actions. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, feedback, and secrets.", + "migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics, Windows/network-share-safe plugin code replacement, and clobber-safe shared form submission. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, feedback, and secrets.", "rollback_safe": true, "requirements": [ "Node.js 18 or newer" @@ -121,6 +121,18 @@ ], "rollback_safe": true, "migration_notes": "Fixes Lumi AI feedback review actions and failure reporting; existing feedback, corrections, models, settings, and plugin data are preserved." + }, + { + "version": "0.2.9", + "channel": "stable", + "released_at": "2026-07-18", + "compatible_from": "0.1.9", + "migration_kind": "patch", + "replaces_versions": [ + "1.2.0" + ], + "rollback_safe": true, + "migration_notes": "Fixes shared browser form endpoint shadowing and Lumi AI feedback 404 responses; existing feedback, settings, models, corrections, and plugin data are preserved." } ] }