fix(connection): keep equals readable in copied connection urls

This commit is contained in:
Xsyl.way
2026-10-15 19:28:27 +08:00
committed by GitHub
parent 4a5d1c78c1
commit 12dfcb424b
2 changed files with 44 additions and 4 deletions
@@ -17,7 +17,10 @@ function config(overrides: Partial<ConnectionUrlCopyConfig>): ConnectionUrlCopyC
}
const ASCII_SYMBOLS = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
const ENCODED_ASCII_SYMBOLS = "%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~";
// `=` stays literal (issue #12304): userinfo and query values split on their
// first separator, so escaping it only made copied passwords differ from the
// stored credential for clients that never percent-decode.
const ENCODED_ASCII_SYMBOLS = "%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3C=%3E%3F%40%5B%5C%5D%5E_%60%7B%7C%7D~";
describe("connectionUrlCopyFormats", () => {
it("lists all formats for a PostgreSQL connection with a stored password", () => {
@@ -104,6 +107,20 @@ describe("buildConnectionUrlCopy standard URL", () => {
expect(buildConnectionUrlCopy(config({ password: "W^j.U" }), "urlWithPassword")).toBe("postgresql://app_user:W%5Ej.U@db.example.com:5432/appdb");
});
it("keeps `=` readable in copied URLs instead of escaping it as %3D", () => {
const password = "p=ss=word";
const mysql = config({ db_type: "mysql", port: 3306, password });
const mysqlUrl = buildConnectionUrlCopy(mysql, "urlWithPassword");
expect(mysqlUrl).toBe("mysql://app_user:p=ss=word@db.example.com:3306/appdb");
expect(parseConnectionUrl(mysqlUrl ?? "").password).toBe(password);
const redis = config({ db_type: "redis", port: 6379, username: "", password, database: "0" });
expect(buildConnectionUrlCopy(redis, "urlWithPassword")).toBe("redis://:p=ss=word@db.example.com:6379/0");
const oceanbase = config({ db_type: "mysql", port: 2883, driver_profile: "oceanbase", password });
expect(buildConnectionUrlCopy(oceanbase, "urlWithPassword")).toBe("oceanbase://app_user:p=ss=word@db.example.com:2883/appdb");
});
it("preserves credentials that already look percent-encoded", () => {
const password = "%25%40%5E";
const text = buildConnectionUrlCopy(config({ password }), "urlWithPassword");
@@ -193,6 +210,19 @@ describe("buildConnectionUrlCopy JDBC URL", () => {
expect(parseConnectionUrl(text ?? "").password).toBe(password);
});
it("keeps `=` readable in MySQL-family JDBC credentials", () => {
const password = "p=ss=word";
const mysql = config({ db_type: "mysql", port: 3306, password });
const mysqlUrl = buildConnectionUrlCopy(mysql, "jdbcUrlWithCredentials");
expect(mysqlUrl).toBe("jdbc:mysql://db.example.com:3306/appdb?user=app_user&password=p=ss=word");
expect(parseConnectionUrl(mysqlUrl ?? "").password).toBe(password);
const oceanbase = config({ db_type: "mysql", port: 2883, driver_profile: "oceanbase", password });
const oceanbaseUrl = buildConnectionUrlCopy(oceanbase, "jdbcUrlWithCredentials");
expect(oceanbaseUrl).toBe("jdbc:oceanbase://db.example.com:2883/appdb?user=app_user&password=p=ss=word");
expect(parseConnectionUrl(oceanbaseUrl ?? "").password).toBe(password);
});
it("keeps Oracle Easy Connect credentials literal instead of URI-encoding them", () => {
const oracle = config({ db_type: "oracle", host: "ora.example.com", port: 1521, database: "ORCLPDB1", password: "p^ss%40#y" });
expect(buildConnectionUrlCopy(oracle, "jdbcUrlWithCredentials")).toBe("jdbc:oracle:thin:@//ora.example.com:1521/ORCLPDB1?user=app_user&password=p^ss%40#y");
@@ -124,6 +124,9 @@ const POSTGRES_WIRE_DB_TYPES = new Set<DatabaseType>(["postgres", "redshift", "g
/** PG-wire families whose JDBC driver accepts the `ssl`/`sslmode` properties. */
const PG_JDBC_PREFIXES = new Set(["jdbc:postgresql", "jdbc:redshift", "jdbc:gaussdb", "jdbc:opengauss", "jdbc:kingbase8", "jdbc:highgo"]);
/** MySQL-family prefixes that share Connector/J's `?key=value&...` query grammar. */
const MYSQL_QUERY_JDBC_PREFIXES = new Set(["jdbc:mysql", "jdbc:mariadb", "jdbc:oceanbase"]);
function normalizedDriverProfile(config: ConnectionUrlCopyConfig): string {
return config.driver_profile?.trim().toLowerCase() ?? "";
}
@@ -188,9 +191,16 @@ function shouldAppendPort(config: ConnectionUrlCopyConfig): boolean {
* RFC 3986 component encoding for credentials. `encodeURIComponent` leaves
* `!'()*` unescaped, so encode those too; only unreserved characters remain
* literal and every encoded credential can be decoded back byte-for-byte.
*
* `=` is also kept literal: userinfo and query values split on their first
* separator, so an embedded `=` cannot change how the string is read, while
* escaping it made copied passwords differ from the stored one for clients
* that never percent-decode (#12304).
*/
function encodeCredential(value: string): string {
return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
return encodeURIComponent(value)
.replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`)
.replace(/%3D/g, "=");
}
/**
@@ -432,13 +442,13 @@ function buildSpecialJdbcUrl(config: ConnectionUrlCopyConfig, database: string,
function jdbcQuery(config: ConnectionUrlCopyConfig, prefix: string, withCredentials: boolean): string {
const raw = (config.url_params ?? "").trim().replace(/^[?&]+/, "");
// Strict encoding is enabled only for verified query-style drivers.
const encode = prefix === "jdbc:mysql" || prefix === "jdbc:postgresql" ? encodeCredential : encodeURIComponent;
const encode = MYSQL_QUERY_JDBC_PREFIXES.has(prefix) || prefix === "jdbc:postgresql" ? encodeCredential : encodeURIComponent;
const parts = [...jdbcCredentialParams(config, withCredentials, encode)];
if (raw) parts.push(raw);
if (config.ssl) {
if (PG_JDBC_PREFIXES.has(prefix) && !queryHasParam(raw, ["ssl", "sslmode"])) {
parts.push("ssl=true");
} else if ((prefix === "jdbc:mysql" || prefix === "jdbc:mariadb" || prefix === "jdbc:oceanbase") && !queryHasParam(raw, ["usessl", "sslmode", "requiressl"])) {
} else if (MYSQL_QUERY_JDBC_PREFIXES.has(prefix) && !queryHasParam(raw, ["usessl", "sslmode", "requiressl"])) {
parts.push("sslMode=REQUIRED");
} else if (prefix === "jdbc:clickhouse" && !queryHasParam(raw, ["ssl", "sslmode", "secure"])) {
parts.push("ssl=true");