From 072fe10c3d5bfd2563aad99dbe96523217b3aa51 Mon Sep 17 00:00:00 2001 From: - Normal <80595881+normal88@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:21:31 +0300 Subject: [PATCH] fix(config): handle special characters in connection string credentials - URI: split user/password on the first ':' only and percent-decode them - URI query params and key=value format: split on the first '=' only --- src/config.ts | 31 ++++++++++++++++++++++++------- tests/unit/config.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/config.ts b/src/config.ts index bc3bd892..571a9c1f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -45,6 +45,23 @@ export const mysql_transaction_isolation_level = getIsolationLevelStatement( GetConvarInt('mysql_transaction_isolation_level', 2), ); +// split on the first separator only, so values (e.g. passwords) may contain it +function splitOnce(str: string, separator: string): [string, string | undefined] { + const index = str.indexOf(separator); + return index === -1 ? [str, undefined] : [str.slice(0, index), str.slice(index + 1)]; +} + +function decodeUriComponent(str?: string) { + if (!str) return str; + + try { + return decodeURIComponent(str); + } catch { + // not valid percent-encoding (e.g. a literal '%'); keep the raw value + return str; + } +} + function parseUri(connectionString: string) { const splitMatchGroups = connectionString.match( new RegExp( @@ -54,18 +71,18 @@ function parseUri(connectionString: string) { if (!splitMatchGroups) throw new Error(`mysql_connection_string structure was invalid (${connectionString})`); - const authTarget = splitMatchGroups[2] ? splitMatchGroups[2].split(':') : []; + const [user, password] = splitMatchGroups[2] ? splitOnce(splitMatchGroups[2], ':') : []; const options = { - user: authTarget[0] || undefined, - password: authTarget[1] || undefined, + user: decodeUriComponent(user) || undefined, + password: decodeUriComponent(password) || undefined, host: splitMatchGroups[3], port: parseInt(splitMatchGroups[4]), database: splitMatchGroups[5]?.replace(/^\/+/, ''), ...(splitMatchGroups[6] && splitMatchGroups[6].split('&').reduce>((connectionInfo, parameter) => { - const [key, value] = parameter.split('='); - connectionInfo[key] = value; + const [key, value] = splitOnce(parameter, '='); + connectionInfo[key] = value as string; return connectionInfo; }, {})), }; @@ -85,8 +102,8 @@ export function getConnectionOptions(connectionString: string = mysql_connection .replace(/(?:db)=/gi, 'database=') .split(';') .reduce>((connectionInfo, parameter) => { - const [key, value] = parameter.split('='); - if (key) connectionInfo[key] = value; + const [key, value] = splitOnce(parameter, '='); + if (key) connectionInfo[key] = value as string; return connectionInfo; }, {}); diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 892843a1..7014059d 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -38,6 +38,31 @@ describe('getConnectionOptions', () => { expect(opts.database).toBe('mydb'); }); + test('keeps colons in a URI password', () => { + const opts = config.getConnectionOptions('mysql://root:pa:ss@localhost/mydb') as any; + expect(opts).toMatchObject({ user: 'root', password: 'pa:ss', host: 'localhost', database: 'mydb' }); + }); + + test('decodes percent-encoded URI credentials', () => { + const opts = config.getConnectionOptions('mysql://us%40er:p%40ss%23w%2Frd%3F@localhost/mydb') as any; + expect(opts).toMatchObject({ user: 'us@er', password: 'p@ss#w/rd?', host: 'localhost', database: 'mydb' }); + }); + + test('keeps URI credentials that are not valid percent-encoding as-is', () => { + const opts = config.getConnectionOptions('mysql://root:100%@localhost/mydb') as any; + expect(opts.password).toBe('100%'); + }); + + test('keeps equals signs in URI query parameter values', () => { + const opts = config.getConnectionOptions('mysql://localhost/mydb?charset=utf8mb4&foo=a=b') as any; + expect(opts).toMatchObject({ charset: 'utf8mb4', foo: 'a=b' }); + }); + + test('keeps equals signs in a key-value password', () => { + const opts = config.getConnectionOptions('host=localhost;user=root;password=ab=cd==;database=app') as any; + expect(opts).toMatchObject({ host: 'localhost', user: 'root', password: 'ab=cd==', database: 'app' }); + }); + test('normalises key-value aliases (server/uid/pwd/db) to canonical keys', () => { const opts = config.getConnectionOptions('server=127.0.0.1;uid=root;pwd=secret;db=app') as any; expect(opts).toMatchObject({ host: '127.0.0.1', user: 'root', password: 'secret', database: 'app' });