fix(gateway): move plugin HTTP routes before Control UI SPA catch-all#31885
fix(gateway): move plugin HTTP routes before Control UI SPA catch-all#31885steipete merged 2 commits intoopenclaw:mainfrom
Conversation
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟠 Plugin HTTP routes can shadow Control UI paths due to reordered request stages
Description
This changes the previous guarantee (tested as “does not let plugin handlers shadow control ui routes”) and makes it possible for a plugin-registered HTTP route to intercept and respond on Control UI paths (e.g. Impact (if plugins are untrusted/third-party, as implied by existing comments about “untrusted plugins”):
Vulnerable ordering introduced: // Plugins now run before control-ui-avatar/control-ui-http
requestStages.push(
...buildPluginRequestStages({ /* ... */ })
);
if (controlUiEnabled) {
requestStages.push({ name: "control-ui-avatar", /* ... */ });
requestStages.push({ name: "control-ui-http", /* ... */ });
}Additionally, plugin route registration/matching does not constrain paths to a safe namespace; routes can be any absolute path string, so RecommendationConstrain plugin HTTP handling so it cannot intercept Control UI routes. Options (choose one, in order of safety):
Example approach (2), checking if a plugin route matches before falling into the Control UI catch-all: // Pseudocode: only run plugin handler before Control UI when the request is a registered plugin path.
if (handlePluginRequest && isRegisteredPluginHttpRoutePath(pluginRegistry, requestPath)) {
requestStages.push(...buildPluginRequestStages(...));
}
// then run control UIThis preserves Control UI integrity while keeping explicit plugin endpoints reachable. Analyzed PR: #31885 at commit Last updated on: 2026-03-02T18:37:42Z |
Greptile SummaryThis PR fixes a bug where plugin HTTP routes registered via The fix reorders the handler chain in
The implementation correctly handles the fallthrough logic - plugin handlers are called first, and if they return false, the request proceeds to the Control UI handlers as expected. Tests verify both that plugins can now handle custom paths AND that unhandled requests still reach the Control UI. Trade-off acknowledged: Plugins can now intercept Control UI paths (e.g., Confidence Score: 4/5
Last reviewed commit: 10854dc |
The Control UI handler (`handleControlUiHttpRequest`) acts as an SPA catch-all that matches every path, returning HTML for GET requests and 405 for other methods. Because it ran before `handlePluginRequest` in the request chain, any plugin HTTP route that did not live under `/plugins` or `/api` was unreachable — shadowed by the catch-all. Reorder the handlers so plugin routes are evaluated first. Core built-in routes (hooks, tools, Slack, Canvas, etc.) still take precedence because they are checked even earlier in the chain. Unmatched plugin paths continue to fall through to Control UI as before. Closes openclaw#31766
10854dc to
36ba09e
Compare
…openclaw#31885) * fix(gateway): move plugin HTTP routes before Control UI SPA catch-all The Control UI handler (`handleControlUiHttpRequest`) acts as an SPA catch-all that matches every path, returning HTML for GET requests and 405 for other methods. Because it ran before `handlePluginRequest` in the request chain, any plugin HTTP route that did not live under `/plugins` or `/api` was unreachable — shadowed by the catch-all. Reorder the handlers so plugin routes are evaluated first. Core built-in routes (hooks, tools, Slack, Canvas, etc.) still take precedence because they are checked even earlier in the chain. Unmatched plugin paths continue to fall through to Control UI as before. Closes openclaw#31766 * fix: add changelog for plugin route precedence landing (openclaw#31885) (thanks @Sid-Qin) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
…openclaw#31885) * fix(gateway): move plugin HTTP routes before Control UI SPA catch-all The Control UI handler (`handleControlUiHttpRequest`) acts as an SPA catch-all that matches every path, returning HTML for GET requests and 405 for other methods. Because it ran before `handlePluginRequest` in the request chain, any plugin HTTP route that did not live under `/plugins` or `/api` was unreachable — shadowed by the catch-all. Reorder the handlers so plugin routes are evaluated first. Core built-in routes (hooks, tools, Slack, Canvas, etc.) still take precedence because they are checked even earlier in the chain. Unmatched plugin paths continue to fall through to Control UI as before. Closes openclaw#31766 * fix: add changelog for plugin route precedence landing (openclaw#31885) (thanks @Sid-Qin) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
…openclaw#31885) * fix(gateway): move plugin HTTP routes before Control UI SPA catch-all The Control UI handler (`handleControlUiHttpRequest`) acts as an SPA catch-all that matches every path, returning HTML for GET requests and 405 for other methods. Because it ran before `handlePluginRequest` in the request chain, any plugin HTTP route that did not live under `/plugins` or `/api` was unreachable — shadowed by the catch-all. Reorder the handlers so plugin routes are evaluated first. Core built-in routes (hooks, tools, Slack, Canvas, etc.) still take precedence because they are checked even earlier in the chain. Unmatched plugin paths continue to fall through to Control UI as before. Closes openclaw#31766 * fix: add changelog for plugin route precedence landing (openclaw#31885) (thanks @Sid-Qin) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
…openclaw#31885) * fix(gateway): move plugin HTTP routes before Control UI SPA catch-all The Control UI handler (`handleControlUiHttpRequest`) acts as an SPA catch-all that matches every path, returning HTML for GET requests and 405 for other methods. Because it ran before `handlePluginRequest` in the request chain, any plugin HTTP route that did not live under `/plugins` or `/api` was unreachable — shadowed by the catch-all. Reorder the handlers so plugin routes are evaluated first. Core built-in routes (hooks, tools, Slack, Canvas, etc.) still take precedence because they are checked even earlier in the chain. Unmatched plugin paths continue to fall through to Control UI as before. Closes openclaw#31766 * fix: add changelog for plugin route precedence landing (openclaw#31885) (thanks @Sid-Qin) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
Summary
api.registerHttpRoute()are unreachable in 2026.3.1 becausehandleControlUiHttpRequest(SPA catch-all) runs beforehandlePluginRequestin the gateway request chain, returning 405 for POST or HTML for GET on any path./pluginsor/apicannot receive requests — they are silently shadowed by the Control UI.createGatewayHttpServersohandlePluginRequestruns beforehandleControlUiHttpRequest. Core built-in routes (hooks, tools, Slack, Canvas) still take precedence because they are checked even earlier.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
api.registerHttpRoute()are now reachable (previously returned 405/HTML).Security Impact (required)
NoNoNoNoNoRepro + Verification
Environment
Steps
/my-plugin/inbound)curl -X POST http://127.0.0.1:18789/my-plugin/inboundExpected
Actual
Evidence
Updated test:
"plugin routes take priority over control ui catch-all"verifies the new behavior.Added test:
"unmatched plugin paths fall through to control ui"verifies Control UI still handles non-plugin paths.Human Verification (required)
Compatibility / Migration
YesNoNoFailure Recovery (if this breaks)
server-http.tssrc/gateway/server-http.tsRisks and Mitigations
A plugin that registers a handler for a Control UI path (e.g.
/chat) will now intercept those requests. This is intentional — explicitly registered routes should take priority over a catch-all.