Parameter injection in DB connection URIs leading to local file inclusion in WhoDB
Description
WhoDB is an open source database management tool. In affected versions the application is vulnerable to parameter injection in database connection strings, which allows an attacker to read local files on the machine the application is running on. The application uses string concatenation to build database connection URIs which are then passed to corresponding libraries responsible for setting up the database connections. This string concatenation is done unsafely and without escaping or encoding the user input. This allows an user, in many cases, to inject arbitrary parameters into the URI string. These parameters can be potentially dangerous depending on the libraries used. One of these dangerous parameters is allowAllFiles in the library github.com/go-sql-driver/mysql. Should this be set to true, the library enables running the LOAD DATA LOCAL INFILE query on any file on the host machine (in this case, the machine that WhoDB is running on). By injecting &allowAllFiles=true into the connection URI and connecting to any MySQL server (such as an attacker-controlled one), the attacker is able to read local files. This issue has been addressed in version 0.45.0 and all users are advised to upgrade. There are no known workarounds for this vulnerability.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/clidey/whodb/coreGo | < 0.0.0-20250127202645-8d67b767e005 | 0.0.0-20250127202645-8d67b767e005 |
Affected products
1Patches
111 files changed · +178 −98
core/src/engine/plugin.go+1 −0 modified@@ -9,6 +9,7 @@ type Credentials struct { Database string Advanced []Record AccessToken *string + IsProfile bool } type ExternalModel struct {
core/src/env/env.go+2 −1 modified@@ -41,7 +41,8 @@ type DatabaseCredentials struct { Port string `json:"port"` Config map[string]string `json:"config"` - Type string + IsProfile bool + Type string } func GetDefaultDatabaseCredentials(databaseType string) []DatabaseCredentials {
core/src/plugins/clickhouse/db.go+33 −24 modified@@ -5,8 +5,8 @@ import ( "crypto/tls" "database/sql" "fmt" - "github.com/clidey/whodb/core/src/log" - "strings" + "net" + "strconv" "time" "github.com/ClickHouse/clickhouse-go/v2" @@ -24,21 +24,38 @@ const ( ) func DB(config *engine.PluginConfig) (*sql.DB, error) { - port := common.GetRecordValueOrDefault(config.Credentials.Advanced, portKey, "9000") + port, err := strconv.Atoi(common.GetRecordValueOrDefault(config.Credentials.Advanced, portKey, "9000")) + if err != nil { + return nil, err + } sslMode := common.GetRecordValueOrDefault(config.Credentials.Advanced, sslModeKey, "disable") httpProtocol := common.GetRecordValueOrDefault(config.Credentials.Advanced, httpProtocolKey, "disable") readOnly := common.GetRecordValueOrDefault(config.Credentials.Advanced, readOnlyKey, "disable") debug := common.GetRecordValueOrDefault(config.Credentials.Advanced, debugKey, "disable") + + auth := clickhouse.Auth{ + Database: config.Credentials.Database, + Username: config.Credentials.Username, + Password: config.Credentials.Password, + } + address := []string{net.JoinHostPort(config.Credentials.Hostname, strconv.Itoa(port))} options := &clickhouse.Options{ - Addr: []string{fmt.Sprintf("%s:%s", config.Credentials.Hostname, port)}, - Auth: clickhouse.Auth{ - Database: config.Credentials.Database, - Username: config.Credentials.Username, - Password: config.Credentials.Password, - }, + Addr: address, + Auth: auth, DialTimeout: time.Second * 30, ConnOpenStrategy: clickhouse.ConnOpenInOrder, + Compression: &clickhouse.Compression{ + Method: clickhouse.CompressionLZ4, + }, + } + + if httpProtocol != "disable" { + options.Protocol = clickhouse.HTTP + options.Compression = &clickhouse.Compression{ + Method: clickhouse.CompressionGZIP, + } } + if debug != "disable" { options.Debug = true } @@ -47,27 +64,19 @@ func DB(config *engine.PluginConfig) (*sql.DB, error) { "max_execution_time": 60, } } - if strings.HasPrefix(port, "8") || httpProtocol != "disable" { - options.Protocol = clickhouse.HTTP - options.Compression = &clickhouse.Compression{ - Method: clickhouse.CompressionGZIP, - } - } else { - options.Compression = &clickhouse.Compression{ - Method: clickhouse.CompressionLZ4, - } - options.MaxOpenConns = 5 - options.MaxIdleConns = 5 - options.ConnMaxLifetime = time.Hour - } if sslMode != "disable" { options.TLS = &tls.Config{InsecureSkipVerify: sslMode == "relaxed" || sslMode == "none"} } conn := clickhouse.OpenDB(options) - err := conn.PingContext(context.Background()) + + conn.SetMaxOpenConns(5) + conn.SetMaxOpenConns(5) + conn.SetConnMaxLifetime(time.Hour) + + err = conn.PingContext(context.Background()) if err != nil { - log.Logger.Warnf("clickhouse.Ping() error: %v", err) + return nil, err } return conn, err }
core/src/plugins/elasticsearch/db.go+24 −11 modified@@ -2,6 +2,9 @@ package elasticsearch import ( "fmt" + "net" + "net/url" + "strconv" "github.com/clidey/whodb/core/src/common" "github.com/clidey/whodb/core/src/engine" @@ -10,22 +13,32 @@ import ( func DB(config *engine.PluginConfig) (*elasticsearch.Client, error) { var addresses []string - port := common.GetRecordValueOrDefault(config.Credentials.Advanced, "Port", "9200") + port, err := strconv.Atoi(common.GetRecordValueOrDefault(config.Credentials.Advanced, "Port", "9200")) + if err != nil { + return nil, err + } sslMode := common.GetRecordValueOrDefault(config.Credentials.Advanced, "SSL Mode", "disable") - if sslMode == "enable" { - addresses = []string{ - fmt.Sprintf("https://%s:%s", config.Credentials.Hostname, port), - } - } else { - addresses = []string{ - fmt.Sprintf("http://%s:%s", config.Credentials.Hostname, port), - } + + hostName := url.QueryEscape(config.Credentials.Hostname) + + scheme := "https" + if sslMode == "disable" { + scheme = "http" + } + + addressUrl := url.URL{ + Scheme: scheme, + Host: net.JoinHostPort(hostName, strconv.Itoa(port)), + } + + addresses = []string{ + addressUrl.String(), } cfg := elasticsearch.Config{ Addresses: addresses, - Username: config.Credentials.Username, - Password: config.Credentials.Password, + Username: url.QueryEscape(config.Credentials.Username), + Password: url.QueryEscape(config.Credentials.Password), } client, err := elasticsearch.NewClient(cfg)
core/src/plugins/mongodb/db.go+26 −18 modified@@ -3,37 +3,45 @@ package mongodb import ( "context" "fmt" - "github.com/clidey/whodb/core/src/common" "github.com/clidey/whodb/core/src/engine" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + "net/url" + "strconv" + "strings" ) func DB(config *engine.PluginConfig) (*mongo.Client, error) { ctx := context.Background() - port := common.GetRecordValueOrDefault(config.Credentials.Advanced, "Port", "27017") + port, err := strconv.Atoi(common.GetRecordValueOrDefault(config.Credentials.Advanced, "Port", "27017")) + if err != nil { + return nil, err + } queryParams := common.GetRecordValueOrDefault(config.Credentials.Advanced, "URL Params", "") dnsEnabled := common.GetRecordValueOrDefault(config.Credentials.Advanced, "DNS Enabled", "false") - var connectionString string - if dnsEnabled == "false" { - connectionString = fmt.Sprintf("mongodb://%s:%s@%s:%s/%s%s", - config.Credentials.Username, - config.Credentials.Password, - config.Credentials.Hostname, - port, - config.Credentials.Database, - queryParams) + + connectionURI := strings.Builder{} + clientOptions := options.Client() + + if strings.ToLower(dnsEnabled) == "true" { + connectionURI.WriteString("mongodb+srv://") + connectionURI.WriteString(fmt.Sprintf("%s/", config.Credentials.Hostname)) + connectionURI.WriteString(config.Credentials.Database) + connectionURI.WriteString(queryParams) } else { - connectionString = fmt.Sprintf("mongodb+srv://%s:%s@%s/%s%s", - config.Credentials.Username, - config.Credentials.Password, - config.Credentials.Hostname, - config.Credentials.Database, - queryParams) + connectionURI.WriteString("mongodb://") + connectionURI.WriteString(fmt.Sprintf("%s:%d/", config.Credentials.Hostname, port)) + connectionURI.WriteString(config.Credentials.Database) + connectionURI.WriteString(queryParams) } - clientOptions := options.Client().ApplyURI(connectionString) + clientOptions.ApplyURI(connectionURI.String()) + clientOptions.SetAuth(options.Credential{ + Username: url.QueryEscape(config.Credentials.Username), + Password: url.QueryEscape(config.Credentials.Password), + }) + client, err := mongo.Connect(ctx, clientOptions) if err != nil { return nil, err
core/src/plugins/mysql/db.go+38 −16 modified@@ -1,45 +1,67 @@ package mysql import ( - "fmt" + "net" "net/url" + "strconv" + "strings" + "time" "github.com/clidey/whodb/core/src/common" "github.com/clidey/whodb/core/src/engine" + mysqldriver "github.com/go-sql-driver/mysql" "gorm.io/driver/mysql" "gorm.io/gorm" ) const ( portKey = "Port" - charsetKey = "Charset" parseTimeKey = "Parse Time" locKey = "Loc" allowClearTextPasswordsKey = "Allow clear text passwords" - hostPathKey = "Host path" ) +// todo: https://github.com/go-playground/validator +// todo: convert below to their respective types before passing into the configuration. check if it can be done before coming here + func DB(config *engine.PluginConfig) (*gorm.DB, error) { - port := common.GetRecordValueOrDefault(config.Credentials.Advanced, portKey, "3306") - charset := common.GetRecordValueOrDefault(config.Credentials.Advanced, charsetKey, "utf8mb4") + port, err := strconv.Atoi(common.GetRecordValueOrDefault(config.Credentials.Advanced, portKey, "3306")) + if err != nil { + return nil, err + } parseTime := common.GetRecordValueOrDefault(config.Credentials.Advanced, parseTimeKey, "True") - loc := common.GetRecordValueOrDefault(config.Credentials.Advanced, locKey, "Local") + loc, err := time.LoadLocation(common.GetRecordValueOrDefault(config.Credentials.Advanced, locKey, "Local")) + if err != nil { + return nil, err + } allowClearTextPasswords := common.GetRecordValueOrDefault(config.Credentials.Advanced, allowClearTextPasswordsKey, "0") - hostPath := common.GetRecordValueOrDefault(config.Credentials.Advanced, hostPathKey, "/") - params := url.Values{} + mysqlConfig := mysqldriver.Config{ + User: config.Credentials.Username, + Passwd: config.Credentials.Password, + Net: "tcp", + Addr: net.JoinHostPort(config.Credentials.Hostname, strconv.Itoa(port)), + DBName: config.Credentials.Database, + AllowCleartextPasswords: allowClearTextPasswords == "1", + ParseTime: strings.ToLower(parseTime) == "true", + Loc: loc, + } - for _, record := range config.Credentials.Advanced { - switch record.Key { - case portKey, charsetKey, parseTimeKey, locKey, allowClearTextPasswordsKey, hostPathKey: - continue - default: - params.Add(record.Key, fmt.Sprintf("%v", record.Value)) + // if this config is a pre-configured profile, then allow reading of additional params + if config.Credentials.IsProfile { + params := make(map[string]string) + for _, record := range config.Credentials.Advanced { + switch record.Key { + case portKey, parseTimeKey, locKey, allowClearTextPasswordsKey: + continue + default: + params[record.Key] = url.QueryEscape(record.Value) + } } + mysqlConfig.Params = params } - dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)%v%v?charset=%v&parseTime=%v&loc=%v&allowCleartextPasswords=%v&%v", config.Credentials.Username, config.Credentials.Password, config.Credentials.Hostname, port, hostPath, config.Credentials.Database, charset, parseTime, loc, allowClearTextPasswords, params.Encode()) - db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + db, err := gorm.Open(mysql.Open(mysqlConfig.FormatDSN()), &gorm.Config{}) if err != nil { return nil, err }
core/src/plugins/postgres/db.go+29 −12 modified@@ -2,6 +2,7 @@ package postgres import ( "fmt" + "strconv" "strings" "github.com/clidey/whodb/core/src/common" @@ -11,26 +12,42 @@ import ( ) const ( - portKey = "Port" - sslModeKey = "SSL Mode" + portKey = "Port" ) +func escape(x string) string { + return strings.ReplaceAll(x, "'", "\\'") +} + func DB(config *engine.PluginConfig) (*gorm.DB, error) { - port := common.GetRecordValueOrDefault(config.Credentials.Advanced, portKey, "5432") - sslMode := common.GetRecordValueOrDefault(config.Credentials.Advanced, sslModeKey, "disable") + port, err := strconv.Atoi(common.GetRecordValueOrDefault(config.Credentials.Advanced, portKey, "5432")) + if err != nil { + return nil, err + } + host := escape(config.Credentials.Hostname) + username := escape(config.Credentials.Username) + password := escape(config.Credentials.Password) + database := escape(config.Credentials.Database) params := strings.Builder{} - - for _, record := range config.Credentials.Advanced { - switch record.Key { - case portKey, sslModeKey: - continue - default: - params.WriteString(fmt.Sprintf("%v=%v ", record.Key, record.Value)) + if config.Credentials.IsProfile { + for _, record := range config.Credentials.Advanced { + switch record.Key { + case portKey: + continue + default: + params.WriteString(fmt.Sprintf("%v='%v' ", record.Key, escape(record.Value))) + } } } - dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=%v %v", config.Credentials.Hostname, config.Credentials.Username, config.Credentials.Password, config.Credentials.Database, port, sslMode, params.String()) + dsn := fmt.Sprintf("host='%v' user='%v' password='%v' dbname='%v' port='%v'", + host, username, password, database, port) + + if params.Len() > 0 { + dsn = fmt.Sprintf("%v %v", dsn, params.String()) + } + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { return nil, err
core/src/plugins/redis/db.go+6 −3 modified@@ -2,7 +2,7 @@ package redis import ( "context" - "fmt" + "net" "strconv" "github.com/clidey/whodb/core/src/common" @@ -12,7 +12,10 @@ import ( func DB(config *engine.PluginConfig) (*redis.Client, error) { ctx := context.Background() - port := common.GetRecordValueOrDefault(config.Credentials.Advanced, "Port", "6379") + port, err := strconv.Atoi(common.GetRecordValueOrDefault(config.Credentials.Advanced, "Port", "6379")) + if err != nil { + return nil, err + } database := 0 if config.Credentials.Database != "" { var err error @@ -21,7 +24,7 @@ func DB(config *engine.PluginConfig) (*redis.Client, error) { return nil, err } } - addr := fmt.Sprintf("%s:%s", config.Credentials.Hostname, port) + addr := net.JoinHostPort(config.Credentials.Hostname, strconv.Itoa(port)) client := redis.NewClient(&redis.Options{ Addr: addr, Password: config.Credentials.Password,
core/src/src.go+8 −6 modified@@ -39,6 +39,7 @@ func GetLoginProfiles() []env.DatabaseCredentials { databaseProfiles := env.GetDefaultDatabaseCredentials(string(plugin.Type)) for _, databaseProfile := range databaseProfiles { databaseProfile.Type = string(plugin.Type) + databaseProfile.IsProfile = true profiles = append(profiles, databaseProfile) } } @@ -66,11 +67,12 @@ func GetLoginCredentials(profile env.DatabaseCredentials) *engine.Credentials { } return &engine.Credentials{ - Type: profile.Type, - Hostname: profile.Hostname, - Username: profile.Username, - Password: profile.Password, - Database: profile.Database, - Advanced: advanced, + Type: profile.Type, + Hostname: profile.Hostname, + Username: profile.Username, + Password: profile.Password, + Database: profile.Database, + Advanced: advanced, + IsProfile: profile.IsProfile, } }
dev/docker-compose.yml+5 −0 modified@@ -94,6 +94,11 @@ services: ports: - '8123:8123' - '9000:9000' + environment: + CLICKHOUSE_USER: user + CLICKHOUSE_PASSWORD: password + CLICKHOUSE_DB: database + CLICKHOUSE_ALWAYS_RUN_INITDB_SCRIPTS: true volumes: - clickhouse:/var/lib/clickhouse networks:
frontend/src/pages/auth/login.tsx+6 −7 modified@@ -2,14 +2,13 @@ import classNames from "classnames"; import { entries } from "lodash"; import { cloneElement, FC, ReactElement, useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; -import { twMerge } from "tailwind-merge"; import { AnimatedButton } from "../../components/button"; -import { BASE_CARD_CLASS, BRAND_COLOR } from "../../components/classes"; +import { BRAND_COLOR } from "../../components/classes"; import { createDropdownItem, DropdownWithLabel, IDropdownItem } from "../../components/dropdown"; import { Icons } from "../../components/icons"; import { InputWithlabel } from "../../components/input"; import { Loading } from "../../components/loading"; -import { Container, Page } from "../../components/page"; +import { Container } from "../../components/page"; import { InternalRoutes } from "../../config/routes"; import { DatabaseType, LoginCredentials, useGetDatabaseLazyQuery, useGetProfilesQuery, useLoginMutation, useLoginWithProfileMutation } from '../../generated/graphql'; import { AuthActions } from "../../store/auth"; @@ -22,19 +21,19 @@ const databaseTypeDropdownItems: IDropdownItem<Record<string, string>>[] = [ id: "Postgres", label: "Postgres", icon: Icons.Logos.Postgres, - extra: {"Port": "5432", "SSL Mode": "disable",}, + extra: {"Port": "5432"}, }, { id: "MySQL", label: "MySQL", icon: Icons.Logos.MySQL, - extra: {"Port": "3306", "Charset": "utf8mb4", "Parse Time": "True", "Loc": "Local", "Allow clear text passwords": "0", "Host path": "/"}, + extra: {"Port": "3306", "Parse Time": "True", "Loc": "Local", "Allow clear text passwords": "0"}, }, { id: "MariaDB", label: "MariaDB", icon: Icons.Logos.MariaDB, - extra: {"Port": "3306", "Charset": "utf8mb4", "Parse Time": "True", "Loc": "Local", "Allow clear text passwords": "0"}, + extra: {"Port": "3306", "Parse Time": "True", "Loc": "Local", "Allow clear text passwords": "0"}, }, { id: "Sqlite3", @@ -66,7 +65,7 @@ const databaseTypeDropdownItems: IDropdownItem<Record<string, string>>[] = [ extra: { "Port": "9000", "SSL mode": "disable", - "HTTP protocol": "disable", + "HTTP Protocol": "disable", "Readonly": "disable", "Debug": "disable" }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-c7w4-9wv8-7x7cghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-24787ghsaADVISORY
- github.com/clidey/whodb/commit/8d67b767e00552e5eba2b1537179b74bfa662ee1ghsaWEB
- github.com/clidey/whodb/security/advisories/GHSA-c7w4-9wv8-7x7cghsax_refsource_CONFIRMWEB
- github.com/go-sql-driver/mysql/blob/7403860363ca112af503b4612568c3096fecb466/infile.goghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.