fix(gateway): prevent /api/* routes from returning SPA HTML when basePath is empty#30333
Conversation
Greptile SummaryThis PR fixes a real bug where Key observations:
Confidence Score: 3/5
Last reviewed commit: ab21f3e |
Additional Comments (1)
The method-not-allowed guard at line 278 fires for every non- Repro: The fix only covers To cover all methods, the // Proposed reordering (sketch — not a drop-in suggestion block):
if (!basePath && (pathname === "/api" || pathname.startsWith("/api/"))) {
return false;
}
if (req.method !== "GET" && req.method !== "HEAD") {
res.statusCode = 405;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Method Not Allowed");
return true;
}Note: the method guard should still reject non- Prompt To Fix With AIThis is a comment left during a code review.
Path: src/gateway/control-ui.ts
Line: 278-297
Comment:
**`POST`/`PUT`/`DELETE` to `/api/*` still intercepted with 405**
The method-not-allowed guard at line 278 fires for every non-`GET`/`HEAD` request **before** the new `/api` early-return is reached. As a result, `POST /api/sessions` (and any other mutating API call) still returns `405 Method Not Allowed` from the Control UI handler instead of falling through to the real API handler.
Repro:
```
curl -X POST http://127.0.0.1:18789/api/sessions
# → 405 Method Not Allowed (from Control UI, not API layer)
```
The fix only covers `GET` and `HEAD`. Any API client (including the Discord integration mentioned in the PR description) that uses `POST`, `PUT`, `PATCH`, or `DELETE` against `/api/*` remains broken.
To cover all methods, the `/api` check needs to happen before the method guard:
```typescript
// Proposed reordering (sketch — not a drop-in suggestion block):
if (!basePath && (pathname === "/api" || pathname.startsWith("/api/"))) {
return false;
}
if (req.method !== "GET" && req.method !== "HEAD") {
res.statusCode = 405;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Method Not Allowed");
return true;
}
```
Note: the method guard should still reject non-`GET`/`HEAD` requests to Control UI asset paths (`/ui/`, SPA fallback, etc.), so the condition either needs to remain in place for those paths or the early-return for API paths simply prevents the guard from being relevant for them.
How can I resolve this? If you propose a fix, please make it concise. |
ab21f3e to
04584e8
Compare
🔒 Aisle Security Analysis✅ We scanned this PR and did not find any security vulnerabilities. Aisle supplements but does not replace security review. Analyzed PR: #30333 at commit Last updated on: 2026-03-01T21:47:30Z |
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟡 Control UI /api exclusion can be bypassed via protocol-relative paths and encoded separators (path confusion)
DescriptionThe new
As a result, the Control UI handler may still return Vulnerable code: const url = new URL(urlRaw, "http://localhost");
const pathname = url.pathname;
...
if (pathname === "/api" || pathname.startsWith("/api/")) {
return false;
}RecommendationHarden request-target parsing and apply the
const urlRaw = req.url ?? "/";
if (!urlRaw.startsWith("/")) return false;
// Avoid scheme-relative parsing quirks for leading "//"
const url = new URL(`http://localhost${urlRaw}`);
import { canonicalizePathForSecurity } from "./security-path.js";
const { canonicalPath, candidates } = canonicalizePathForSecurity(url.pathname);
const isApi = candidates.some((p) => p === "/api" || p.startsWith("/api/"));
if (isApi) return false;If you do not want to accept encoded separators at all, explicitly reject requests whose raw path contains Analyzed PR: #30333 at commit Last updated on: 2026-03-01T21:23:12Z |
dbdb26b to
0f908ad
Compare
…Path is empty When controlUiBasePath is unset (empty string), handleControlUiHttpRequest acts as a catch-all and serves index.html for every unmatched path — including /api/* routes. This causes API consumers (browser UI, curl, Discord) to receive an HTML page instead of the expected JSON 404. Add an early return false for /api and /api/* paths in the empty-basePath branch so they fall through to the gateway's normal 404 handler. Closes openclaw#30295
0f908ad to
12591f3
Compare
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
…Path is empty (openclaw#30333) Merged via squash. Prepared head SHA: 12591f3 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
Summary
gateway.controlUi.basePathis unset (empty string, the default), all/api/*HTTP requests return the Control UI SPAindex.htmlinstead of JSON or a proper 404. This breaks the browser UI, curl-based API usage, and Discord integration.src/gateway/control-ui.ts— added an earlyreturn falsefor/apiand/api/*paths in the!basePathbranch ofhandleControlUiHttpRequest, so these paths fall through to the gateway's normal handler chain and ultimately reach the 404 handler.basePathis set (e.g./webchat), the existing guard at line 305 already excludes non-basePath paths. The SPA fallback for legitimate UI routes is unaffected.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
/api/*HTTP requests now correctly return 404 (or the appropriate API response) instead of HTML whenbasePathis empty.Security Impact (required)
NoNoNoNoNoRepro + Verification
Environment
Steps
gateway.controlUi.basePathset)curl http://127.0.0.1:18789/api/sessionsExpected
404 Not Found(plain text)Actual
200 OKwith HTML (<!doctype html>...)404 Not Found(plain text)Evidence
Human Verification (required)
/api,/api/sessions,/api/channels/nostrall returnhandled: falsewhen basePath is emptyCompatibility / Migration
YesNoNoFailure Recovery (if this breaks)
src/gateway/control-ui.ts/api/*returns HTML againRisks and Mitigations
None — this restores the expected behavior where the Control UI handler only serves SPA content for UI paths.