mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
Matrix: validate homeserver URLs at runtime
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
resolveMatrixAuthContext,
|
||||
resolveMatrixConfig,
|
||||
resolveMatrixConfigForAccount,
|
||||
validateMatrixHomeserverUrl,
|
||||
} from "./client.js";
|
||||
import * as credentialsModule from "./credentials.js";
|
||||
import * as sdkModule from "./sdk.js";
|
||||
@@ -146,6 +147,13 @@ describe("resolveMatrixConfig", () => {
|
||||
"default",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects insecure public http Matrix homeservers", () => {
|
||||
expect(() => validateMatrixHomeserverUrl("http://matrix.example.org")).toThrow(
|
||||
"Matrix homeserver must use https:// unless it targets a private or loopback host",
|
||||
);
|
||||
expect(validateMatrixHomeserverUrl("http://127.0.0.1:8008")).toBe("http://127.0.0.1:8008");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMatrixAuth", () => {
|
||||
@@ -273,6 +281,21 @@ describe("resolveMatrixAuth", () => {
|
||||
expect(saveMatrixCredentialsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects embedded credentials in Matrix homeserver URLs", async () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
matrix: {
|
||||
homeserver: "https://user:pass@matrix.example.org",
|
||||
accessToken: "tok-123",
|
||||
},
|
||||
},
|
||||
} as CoreConfig;
|
||||
|
||||
await expect(resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow(
|
||||
"Matrix homeserver URL must not include embedded credentials",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to config deviceId when cached credentials are missing it", async () => {
|
||||
vi.mocked(credentialsModule.loadMatrixCredentials).mockReturnValue({
|
||||
homeserver: "https://matrix.example.org",
|
||||
|
||||
@@ -9,6 +9,7 @@ export {
|
||||
resolveImplicitMatrixAccountId,
|
||||
resolveMatrixAuth,
|
||||
resolveMatrixAuthContext,
|
||||
validateMatrixHomeserverUrl,
|
||||
} from "./client/config.js";
|
||||
export { createMatrixClient } from "./client/create-client.js";
|
||||
export {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
isPrivateOrLoopbackHost,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
} from "openclaw/plugin-sdk/account-id";
|
||||
} from "openclaw/plugin-sdk/matrix";
|
||||
import { getMatrixRuntime } from "../../runtime.js";
|
||||
import { normalizeResolvedSecretInputString } from "../../secret-input.js";
|
||||
import type { CoreConfig } from "../../types.js";
|
||||
@@ -142,6 +143,40 @@ export function hasReadyMatrixEnvAuth(config: {
|
||||
return Boolean(homeserver && (accessToken || (userId && password)));
|
||||
}
|
||||
|
||||
export function validateMatrixHomeserverUrl(homeserver: string): string {
|
||||
const trimmed = clean(homeserver, "matrix.homeserver");
|
||||
if (!trimmed) {
|
||||
throw new Error("Matrix homeserver is required (matrix.homeserver)");
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(trimmed);
|
||||
} catch {
|
||||
throw new Error("Matrix homeserver must be a valid http(s) URL");
|
||||
}
|
||||
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
||||
throw new Error("Matrix homeserver must use http:// or https://");
|
||||
}
|
||||
if (!parsed.hostname) {
|
||||
throw new Error("Matrix homeserver must include a hostname");
|
||||
}
|
||||
if (parsed.username || parsed.password) {
|
||||
throw new Error("Matrix homeserver URL must not include embedded credentials");
|
||||
}
|
||||
if (parsed.search || parsed.hash) {
|
||||
throw new Error("Matrix homeserver URL must not include query strings or fragments");
|
||||
}
|
||||
if (parsed.protocol === "http:" && !isPrivateOrLoopbackHost(parsed.hostname)) {
|
||||
throw new Error(
|
||||
"Matrix homeserver must use https:// unless it targets a private or loopback host",
|
||||
);
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function resolveMatrixConfig(
|
||||
cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
@@ -346,9 +381,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
accountId?: string | null;
|
||||
}): Promise<MatrixAuth> {
|
||||
const { cfg, env, accountId, resolved } = resolveMatrixAuthContext(params);
|
||||
if (!resolved.homeserver) {
|
||||
throw new Error("Matrix homeserver is required (matrix.homeserver)");
|
||||
}
|
||||
const homeserver = validateMatrixHomeserverUrl(resolved.homeserver);
|
||||
|
||||
const {
|
||||
loadMatrixCredentials,
|
||||
@@ -361,7 +394,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
const cachedCredentials =
|
||||
cached &&
|
||||
credentialsMatchConfig(cached, {
|
||||
homeserver: resolved.homeserver,
|
||||
homeserver,
|
||||
userId: resolved.userId || "",
|
||||
accessToken: resolved.accessToken,
|
||||
})
|
||||
@@ -379,7 +412,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
if (!userId || !knownDeviceId) {
|
||||
// Fetch whoami when we need to resolve userId and/or deviceId from token auth.
|
||||
ensureMatrixSdkLoggingConfigured();
|
||||
const tempClient = new MatrixClient(resolved.homeserver, resolved.accessToken);
|
||||
const tempClient = new MatrixClient(homeserver, resolved.accessToken);
|
||||
const whoami = (await tempClient.doRequest("GET", "/_matrix/client/v3/account/whoami")) as {
|
||||
user_id?: string;
|
||||
device_id?: string;
|
||||
@@ -404,7 +437,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
if (shouldRefreshCachedCredentials) {
|
||||
await saveMatrixCredentials(
|
||||
{
|
||||
homeserver: resolved.homeserver,
|
||||
homeserver,
|
||||
userId,
|
||||
accessToken: resolved.accessToken,
|
||||
deviceId: knownDeviceId,
|
||||
@@ -417,7 +450,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
}
|
||||
return {
|
||||
accountId,
|
||||
homeserver: resolved.homeserver,
|
||||
homeserver,
|
||||
userId,
|
||||
accessToken: resolved.accessToken,
|
||||
password: resolved.password,
|
||||
@@ -455,7 +488,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
|
||||
// Login with password using the same hardened request path as other Matrix HTTP calls.
|
||||
ensureMatrixSdkLoggingConfigured();
|
||||
const loginClient = new MatrixClient(resolved.homeserver, "");
|
||||
const loginClient = new MatrixClient(homeserver, "");
|
||||
const login = (await loginClient.doRequest("POST", "/_matrix/client/v3/login", undefined, {
|
||||
type: "m.login.password",
|
||||
identifier: { type: "m.id.user", user: resolved.userId },
|
||||
@@ -475,7 +508,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
|
||||
const auth: MatrixAuth = {
|
||||
accountId,
|
||||
homeserver: resolved.homeserver,
|
||||
homeserver,
|
||||
userId: login.user_id ?? resolved.userId,
|
||||
accessToken,
|
||||
password: resolved.password,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import { MatrixClient } from "../sdk.js";
|
||||
import { validateMatrixHomeserverUrl } from "./config.js";
|
||||
import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
|
||||
import {
|
||||
maybeMigrateLegacyStorage,
|
||||
@@ -21,11 +22,12 @@ export async function createMatrixClient(params: {
|
||||
}): Promise<MatrixClient> {
|
||||
ensureMatrixSdkLoggingConfigured();
|
||||
const env = process.env;
|
||||
const homeserver = validateMatrixHomeserverUrl(params.homeserver);
|
||||
const userId = params.userId?.trim() || "unknown";
|
||||
const matrixClientUserId = params.userId?.trim() || undefined;
|
||||
|
||||
const storagePaths = resolveMatrixStoragePaths({
|
||||
homeserver: params.homeserver,
|
||||
homeserver,
|
||||
userId,
|
||||
accessToken: params.accessToken,
|
||||
accountId: params.accountId,
|
||||
@@ -39,14 +41,14 @@ export async function createMatrixClient(params: {
|
||||
|
||||
writeStorageMeta({
|
||||
storagePaths,
|
||||
homeserver: params.homeserver,
|
||||
homeserver,
|
||||
userId,
|
||||
accountId: params.accountId,
|
||||
});
|
||||
|
||||
const cryptoDatabasePrefix = `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`;
|
||||
|
||||
return new MatrixClient(params.homeserver, params.accessToken, undefined, undefined, {
|
||||
return new MatrixClient(homeserver, params.accessToken, undefined, undefined, {
|
||||
userId: matrixClientUserId,
|
||||
password: params.password,
|
||||
deviceId: params.deviceId,
|
||||
|
||||
@@ -68,4 +68,19 @@ describe("probeMatrix", () => {
|
||||
accountId: "ops",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns client validation errors for insecure public http homeservers", async () => {
|
||||
createMatrixClientMock.mockRejectedValue(
|
||||
new Error("Matrix homeserver must use https:// unless it targets a private or loopback host"),
|
||||
);
|
||||
|
||||
const result = await probeMatrix({
|
||||
homeserver: "http://matrix.example.org",
|
||||
accessToken: "tok",
|
||||
timeoutMs: 500,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.error).toContain("Matrix homeserver must use https://");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
getMatrixScopedEnvVarNames,
|
||||
hasReadyMatrixEnvAuth,
|
||||
resolveScopedMatrixEnvConfig,
|
||||
validateMatrixHomeserverUrl,
|
||||
} from "./matrix/client.js";
|
||||
import {
|
||||
resolveMatrixConfigFieldPath,
|
||||
@@ -312,14 +313,12 @@ async function runMatrixConfigure(params: {
|
||||
message: "Matrix homeserver URL",
|
||||
initialValue: existing.homeserver ?? envHomeserver,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
try {
|
||||
validateMatrixHomeserverUrl(String(value ?? ""));
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : "Invalid Matrix homeserver URL";
|
||||
}
|
||||
if (!/^https?:\/\//i.test(raw)) {
|
||||
return "Use a full URL (https://...)";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
).trim();
|
||||
|
||||
@@ -123,6 +123,7 @@ export {
|
||||
resolveMatrixMigrationSnapshotOutputDir,
|
||||
} from "../infra/matrix-migration-snapshot.js";
|
||||
export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
export { isPrivateOrLoopbackHost } from "../gateway/net.js";
|
||||
export {
|
||||
getSessionBindingService,
|
||||
registerSessionBindingAdapter,
|
||||
@@ -142,6 +143,7 @@ export { normalizePollInput } from "../polls.js";
|
||||
export {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "../routing/session-key.js";
|
||||
export type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
Reference in New Issue
Block a user