From 1331d36a11f3713dec3b893e7cb34c9d3746e3cd Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 2 Jun 2026 11:23:10 +0800 Subject: [PATCH] fix(mongodb): normalize URL credentials --- .../connection/ConnectionDialog.vue | 4 ++- apps/desktop/src/lib/connectionUrl.ts | 30 ++++++++++++++++++- packages/app-tests/connectionUrl.test.ts | 17 ++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 8133c69ad5..d4e27f772c 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -15,7 +15,7 @@ import { useToast } from "@/composables/useToast"; import DatabaseIcon from "@/components/icons/DatabaseIcon.vue"; import * as api from "@/lib/api"; import { isTauriRuntime } from "@/lib/tauriRuntime"; -import { applyParsedConnectionUrl, parseConnectionUrl } from "@/lib/connectionUrl"; +import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connectionUrl"; import type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink"; import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation"; import { mongodbAuthFailureHint, mongoUrlParam, setMongoUrlParam } from "@/lib/mongoConnectionOptions"; @@ -894,6 +894,8 @@ function connectionConfigForSubmit(id: string): ConnectionConfig { if (!config.one_time) config.one_time = undefined; if (config.db_type === "mongodb" && !mongoUseUrl.value) { config.connection_string = undefined; + } else if (config.db_type === "mongodb") { + config.connection_string = normalizeMongoConnectionString(config.connection_string?.trim() || ""); } if (config.db_type !== "oracle") { config.oracle_connection_type = undefined; diff --git a/apps/desktop/src/lib/connectionUrl.ts b/apps/desktop/src/lib/connectionUrl.ts index 0026c3fe4d..a94b4e5fc7 100644 --- a/apps/desktop/src/lib/connectionUrl.ts +++ b/apps/desktop/src/lib/connectionUrl.ts @@ -62,6 +62,34 @@ function decodeUrlPart(value: string): string { } } +function decodePercentEscapes(value: string): string { + return value.replace(/%([0-9a-fA-F]{2})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16))); +} + +function encodeMongoUserInfoPart(value: string): string { + return encodeURIComponent(decodePercentEscapes(value)); +} + +export function normalizeMongoConnectionString(value: string): string { + const input = value.trim(); + if (!input) return input; + + let parsed: URL; + try { + parsed = new URL(input); + } catch { + return input; + } + + const scheme = parsed.protocol.replace(/:$/, "").toLowerCase(); + if (scheme !== "mongodb" && scheme !== "mongodb+srv") return input; + if (!parsed.username && !parsed.password) return input; + + const username = encodeMongoUserInfoPart(parsed.username); + const password = parsed.password ? `:${encodeMongoUserInfoPart(parsed.password)}` : ""; + return `${parsed.protocol}//${username}${password}@${parsed.host}${parsed.pathname}${parsed.search}${parsed.hash}`; +} + function databaseFromPath(pathname: string): string | undefined { const value = pathname.replace(/^\/+/, ""); if (!value) return undefined; @@ -304,7 +332,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa database: databaseFromPath(parsed.pathname), urlParams: parsedUrlParams, ssl: scheme === "mongodb+srv", - connectionString: source, + connectionString: normalizeMongoConnectionString(source), useMongoUrl: true, }; } diff --git a/packages/app-tests/connectionUrl.test.ts b/packages/app-tests/connectionUrl.test.ts index 588244aa30..5103b94abb 100644 --- a/packages/app-tests/connectionUrl.test.ts +++ b/packages/app-tests/connectionUrl.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import test from "node:test"; -import { parseConnectionUrl } from "../../apps/desktop/src/lib/connectionUrl.ts"; +import { normalizeMongoConnectionString, parseConnectionUrl } from "../../apps/desktop/src/lib/connectionUrl.ts"; test("parses postgres connection URLs", () => { assert.deepEqual(parseConnectionUrl("postgresql://alice:secret@db.example.com:5433/app?sslmode=require"), { @@ -197,6 +197,21 @@ test("keeps MongoDB URLs as connection strings", () => { assert.equal(parsed.ssl, true); }); +test("normalizes MongoDB URL credentials when reserved characters can be parsed safely", () => { + const parsed = parseConnectionUrl("mongodb://reader:pa@ss:word@mongo.example.com/admin?authSource=admin"); + + assert.equal(parsed.username, "reader"); + assert.equal(parsed.password, "pa@ss:word"); + assert.equal(parsed.connectionString, "mongodb://reader:pa%40ss%3Aword@mongo.example.com/admin?authSource=admin"); +}); + +test("normalizes invalid percent escapes in MongoDB URL credentials", () => { + assert.equal( + normalizeMongoConnectionString("mongodb://reader:pa%ss@mongo.example.com/admin"), + "mongodb://reader:pa%25ss@mongo.example.com/admin", + ); +}); + test("uses selected HTTP-compatible profile for HTTP URLs", () => { const parsed = parseConnectionUrl("https://search.example.com:9243", "elasticsearch");