Skip to content

fix(gateway): move plugin HTTP routes before Control UI SPA catch-all#31885

Merged
steipete merged 2 commits intoopenclaw:mainfrom
Sid-Qin:fix/31766-plugin-route-priority
Mar 2, 2026
Merged

fix(gateway): move plugin HTTP routes before Control UI SPA catch-all#31885
steipete merged 2 commits intoopenclaw:mainfrom
Sid-Qin:fix/31766-plugin-route-priority

Conversation

@Sid-Qin
Copy link
Contributor

@Sid-Qin Sid-Qin commented Mar 2, 2026

Summary

  • Problem: Plugin HTTP routes registered via api.registerHttpRoute() are unreachable in 2026.3.1 because handleControlUiHttpRequest (SPA catch-all) runs before handlePluginRequest in the gateway request chain, returning 405 for POST or HTML for GET on any path.
  • Why it matters: Any plugin that registers webhooks or custom HTTP endpoints outside /plugins or /api cannot receive requests — they are silently shadowed by the Control UI.
  • What changed: Reordered the handler chain in createGatewayHttpServer so handlePluginRequest runs before handleControlUiHttpRequest. Core built-in routes (hooks, tools, Slack, Canvas) still take precedence because they are checked even earlier.
  • What did NOT change: The Control UI still handles all paths not claimed by plugins. Auth enforcement for plugin routes is unchanged.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • Plugin HTTP routes registered via api.registerHttpRoute() are now reachable (previously returned 405/HTML).
  • Plugins CAN now handle paths that would otherwise be caught by the Control UI SPA catch-all.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Linux / macOS
  • Runtime: Node.js v22+
  • Integration/channel: Any plugin with HTTP routes

Steps

  1. Register a plugin HTTP route on a custom path (e.g. /my-plugin/inbound)
  2. curl -X POST http://127.0.0.1:18789/my-plugin/inbound

Expected

  • Plugin handler receives the request, returns 200

Actual

  • Before fix: Control UI returns 405 Method Not Allowed
  • After fix: Plugin handler receives the request correctly

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)

  • Verified scenarios: Plugin routes reachable, unmatched paths fall through to Control UI
  • Edge cases checked: Plugin returning false (not handling) still falls through to Control UI
  • What I did not verify: Production deployment with multiple plugins

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert: Revert the handler order change in server-http.ts
  • Files/config to restore: src/gateway/server-http.ts
  • Known bad symptoms: Plugin accidentally shadowing a Control UI route

Risks 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.

@aisle-research-bot
Copy link

aisle-research-bot bot commented Mar 2, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Plugin HTTP routes can shadow Control UI paths due to reordered request stages

1. 🟠 Plugin HTTP routes can shadow Control UI paths due to reordered request stages

Property Value
Severity High
CWE CWE-284
Location src/gateway/server-http.ts:590-626

Description

createGatewayHttpServer now executes plugin HTTP route handling before the Control UI HTTP handlers.

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. /chat, /avatar/..., or any Control UI SPA route) before the Control UI handler runs.

Impact (if plugins are untrusted/third-party, as implied by existing comments about “untrusted plugins”):

  • UI phishing / spoofing: a plugin can register an exact or prefix route like /chat and serve a lookalike UI.
  • Bypass of Control UI security headers: Control UI applies Content-Security-Policy and X-Frame-Options in applyControlUiSecurityHeaders, but a plugin response will only get the baseline headers from setDefaultSecurityHeaders unless it sets its own.
  • Potential auth-flow abuse: even if Control UI auth is primarily WebSocket-based, a spoofed UI route can trick users into entering tokens/passwords.

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 /chat is a valid plugin route path.

Recommendation

Constrain plugin HTTP handling so it cannot intercept Control UI routes.

Options (choose one, in order of safety):

  1. Namespace plugin HTTP routes (recommended): only allow plugin route registration under a dedicated prefix (e.g. /plugins/<pluginId>/...), and enforce this in registerPluginHttpRoute.

  2. Keep Control UI precedence: move buildPluginRequestStages(...) back after control-ui-avatar and control-ui-http stages, and add a targeted exception so specific plugin endpoints remain reachable (e.g., classify certain webhook paths as not-control-ui, or explicitly check whether a request matches any registered plugin route before the Control UI SPA catch-all).

  3. Blocklist Control UI paths for plugins: reject plugin route registrations that overlap the Control UI base path (including root-mounted /chat, /avatar/*, /assets/*, etc.).

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 UI

This preserves Control UI integrity while keeping explicit plugin endpoints reachable.


Analyzed PR: #31885 at commit 36ba09e

Last updated on: 2026-03-02T18:37:42Z

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR fixes a bug where plugin HTTP routes registered via api.registerHttpRoute() were unreachable in version 2026.3.1. The issue occurred because the Control UI SPA catch-all handler (handleControlUiHttpRequest) executed before the plugin request handler (handlePluginRequest), causing all requests to non-built-in paths to be caught by the Control UI (returning 405 for POST or HTML for GET).

The fix reorders the handler chain in createGatewayHttpServer so plugin routes execute before Control UI handlers:

  • Handler precedence (after change): Built-in routes → Plugin routes → Control UI → Probe endpoints
  • Core behavior preserved: Built-in routes (hooks, tools, Slack, Canvas) still have highest priority
  • Auth unchanged: Plugin route auth enforcement still applies before handler execution
  • Fallthrough works: When plugins return false (not handling), requests continue to Control UI

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., /chat, /avatars/*). This is intentional - explicitly registered plugin routes should take priority over a catch-all handler. However, this means a buggy or malicious plugin could shadow important UI routes, though this risk is mitigated by plugin auth enforcement and the fact that core built-in routes still have highest priority.

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk - the change is well-tested and backward compatible
  • Score reflects solid implementation with comprehensive testing, but acknowledges a notable behavior change where plugins can now shadow Control UI routes. The implementation correctly preserves auth enforcement and fallthrough logic, and tests verify both positive (plugin routes work) and negative (unhandled requests reach Control UI) cases. Core built-in routes maintain highest priority. The intentional trade-off (plugin route priority over catch-all) is reasonable but warrants team awareness for production deployment
  • No files require special attention - both changes are straightforward and well-tested

Last reviewed commit: 10854dc

SidQin-cyber and others added 2 commits March 2, 2026 18:15
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
@steipete steipete force-pushed the fix/31766-plugin-route-priority branch from 10854dc to 36ba09e Compare March 2, 2026 18:16
@steipete steipete merged commit 41c8734 into openclaw:main Mar 2, 2026
10 checks passed
@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: bunx vitest run src/gateway/server.plugin-http-auth.test.ts
  • Land commit: 36ba09e
  • Merge commit: 41c8734

Thanks @Sid-Qin!

execute008 pushed a commit to execute008/openclaw that referenced this pull request Mar 2, 2026
…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>
dawi369 pushed a commit to dawi369/davis that referenced this pull request Mar 3, 2026
…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>
OWALabuy pushed a commit to kcinzgg/openclaw that referenced this pull request Mar 4, 2026
…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>
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[2026.3.1 regression] Control UI handler runs before plugin HTTP routes — plugin routes unreachable

2 participants