openclaw/src/gateway/server/plugins-http.ts
VihariKanukollu cbbe9dd0a2 security: harden credential handling, API auth, and archive extraction
- Control UI: switch token/password from query params to URL fragments (#token=...)
  - Auto-strips after first load, never logged in server access logs
  - Added defense-in-depth headers (Referrer-Policy, X-Frame-Options, CSP, nosniff)
- macOS: "Open Dashboard" now uses fragments instead of query params
- CLI/onboarding: emit fragment links instead of query param links
- Plugin HTTP: /api/** now requires Gateway auth (fixes unauthenticated Nostr API)
  - Added config toggle gateway.plugins.http.protectApiPaths (default: true)
- Control UI: sends Authorization header for Nostr profile save/import
- Android hardening:
  - WebView: disabled mixed content, multi-window, reduced file URL privileges
  - A2UI bridge: origin validation + 64KB payload cap
  - TLS: enabled hostname verification for DNS names
- Archive extraction: block path traversal + symlink/hardlink entries
- Dependencies: upgraded tar 7.5.7, hono 4.11.7, added overrides for vulnerabilities

Breaking: Old ?token=... dashboard links no longer auto-auth; use #token=... instead
2026-01-29 16:05:38 +05:30

88 lines
2.9 KiB
TypeScript

import type { IncomingMessage, ServerResponse } from "node:http";
import type { createSubsystemLogger } from "../../logging/subsystem.js";
import type { PluginRegistry } from "../../plugins/registry.js";
import type { ResolvedGatewayAuth } from "../auth.js";
import { authorizeGatewayConnect } from "../auth.js";
import { sendUnauthorized } from "../http-common.js";
import { getBearerToken, getHeader } from "../http-utils.js";
type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
export type PluginHttpRequestHandler = (
req: IncomingMessage,
res: ServerResponse,
) => Promise<boolean>;
export function createGatewayPluginRequestHandler(params: {
registry: PluginRegistry;
log: SubsystemLogger;
auth?: ResolvedGatewayAuth;
trustedProxies?: string[];
protectApiPaths?: boolean;
}): PluginHttpRequestHandler {
const { registry, log } = params;
return async (req, res) => {
const routes = registry.httpRoutes ?? [];
const handlers = registry.httpHandlers ?? [];
if (routes.length === 0 && handlers.length === 0) return false;
const url = new URL(req.url ?? "/", "http://localhost");
// Security hardening: by default, treat `/api/**` as an authenticated surface.
// Plugins may expose config-mutating endpoints under this namespace.
if (params.protectApiPaths !== false && url.pathname.startsWith("/api/")) {
const token = getBearerToken(req) ?? getHeader(req, "x-moltbot-token")?.trim() ?? "";
const auth = params.auth;
if (!auth) {
sendUnauthorized(res);
return true;
}
const authResult = await authorizeGatewayConnect({
auth,
connectAuth: token ? { token, password: token } : null,
req,
trustedProxies: params.trustedProxies,
});
if (!authResult.ok) {
sendUnauthorized(res);
return true;
}
}
if (routes.length > 0) {
const route = routes.find((entry) => entry.path === url.pathname);
if (route) {
try {
await route.handler(req, res);
return true;
} catch (err) {
log.warn(`plugin http route failed (${route.pluginId ?? "unknown"}): ${String(err)}`);
if (!res.headersSent) {
res.statusCode = 500;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Internal Server Error");
}
return true;
}
}
}
for (const entry of handlers) {
try {
const handled = await entry.handler(req, res);
if (handled) return true;
} catch (err) {
log.warn(`plugin http handler failed (${entry.pluginId}): ${String(err)}`);
if (!res.headersSent) {
res.statusCode = 500;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Internal Server Error");
}
return true;
}
}
return false;
};
}