fix(gateway): let POST requests pass through root-mounted Control UI to plugin handlers#31349
Conversation
Greptile SummaryThis PR fixes a critical bug where plugin webhook POST endpoints were rejected with HTTP 405 when Control UI is root-mounted (the default configuration). Key changes:
Impact:
Test coverage:
Confidence Score: 5/5
Last reviewed commit: dc8fb8b |
2026.3.1 breaks BlueBubbles webhook POST handling (openclaw/openclaw#31344). Watching openclaw/openclaw#31349 for upstream fix.
…to plugin handlers The Control UI handler checked HTTP method before path routing, causing all POST requests (including plugin webhook endpoints like /bluebubbles-webhook) to receive 405 Method Not Allowed. Move the method check after path-based exclusions so non-GET/HEAD requests reach plugin HTTP handlers. Closes openclaw#31344 Made-with: Cursor
dc8fb8b to
0abd3cd
Compare
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Control UI basePath check can be bypassed via URL-encoded slashes, allowing plugin route handling under the UI mount
Description
However, the plugin HTTP route matcher does canonicalize and repeatedly
This mismatch enables route confusion: a request that Control UI treats as outside Vulnerable code: if (basePath) {
if (!pathname.startsWith(`${basePath}/`) && pathname !== basePath) {
return false;
}
// ... 405 for non-GET/HEAD ...
}Relevant downstream canonicalization (for context):
RecommendationMake basePath inclusion checks resilient to encoded path separators so Control UI and plugin routing cannot disagree. A minimal, fail-closed mitigation (mirroring // Treat /openclaw%2F... as under /openclaw to avoid routing confusion.
const underBasePath =
pathname === basePath ||
pathname.startsWith(`${basePath}/`) ||
pathname.startsWith(`${basePath}%`);
if (!underBasePath) return false;Alternatively (more robust), reuse the same canonicalization approach used for plugin security checks and evaluate basePath membership across canonicalized candidates, failing closed on malformed/overly-deep decoding. This ensures paths like 2. 🔵 Open redirect via scheme-relative Control UI basePath in 302 Location header
DescriptionThe Control UI base-path redirect can become an open redirect if
Vulnerable code: res.statusCode = 302;
res.setHeader("Location", `${basePath}/${url.search}`);Impact:
RecommendationReject scheme-relative (and other non-path) base paths during normalization/config validation. For example: export function normalizeControlUiBasePath(basePath?: string): string {
if (!basePath) return "";
let normalized = basePath.trim();
if (!normalized) return "";
if (!normalized.startsWith("/")) normalized = `/${normalized}`;
if (normalized === "/") return "";
if (normalized.endsWith("/")) normalized = normalized.slice(0, -1);
// Reject scheme-relative and other ambiguous paths
if (normalized.startsWith("//")) {
throw new Error("gateway.controlUi.basePath must not start with '//' (scheme-relative URL)");
}
return normalized;
}This ensures Analyzed PR: #31349 at commit Last updated on: 2026-03-02T16:33:31Z |
Summary
handleControlUiHttpRequestchecked the HTTP method (GET/HEADonly) before any path-based routing. When Control UI is root-mounted (nobasePath), every POST request to the gateway — including plugin webhook endpoints like/bluebubbles-webhook— was intercepted with HTTP 405 Method Not Allowed before the plugin handler could run.false(pass-through) instead of 405, allowing plugin HTTP handlers to run. ForbasePath-mounted Control UI, 405 is still returned for confirmed Control UI paths./apiand/pluginsexclusions. The basePath redirect still works. No plugin code was changed.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
/bluebubbles-webhook) now receive requests correctly instead of 405 Method Not Allowed.Security Impact (required)
Repro + Verification
Environment
Steps
channels.bluebubbles.webhookPath: "/bluebubbles-webhook"with default (root-mounted) Control UIcurl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:18789/bluebubbles-webhook -H "Content-Type: application/json" -d '{}'Expected
Actual
Evidence
Three new test cases added:
does not handle POST to root-mounted paths (plugin webhook passthrough)— verifies POST to/bluebubbles-webhook,/custom-webhook,/callbackreturnsfalsereturns 405 for POST to basePath-mounted control UI routes— verifies POST to/openclaw/some-pagestill returns 405does not handle POST to paths outside basePath— verifies POST to/bluebubbles-webhookwith basePath/openclawreturnsfalseHuman Verification (required)
/pluginsand/apiexclusions unchanged; HEAD requests still work; symlink hardening preservedCompatibility / Migration
Failure Recovery (if this breaks)
src/gateway/control-ui.tsto restore the early method checksrc/gateway/control-ui.tsRisks and Mitigations
/random-pathwould fall through to 404 instead of 405)