VYPR
High severityNVD Advisory· Published Feb 6, 2025· Updated Feb 6, 2025

Parameter injection in DB connection URIs leading to local file inclusion in WhoDB

CVE-2025-24787

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.

PackageAffected versionsPatched versions
github.com/clidey/whodb/coreGo
< 0.0.0-20250127202645-8d67b767e0050.0.0-20250127202645-8d67b767e005

Affected products

1

Patches

1
8d67b767e005

Merge commit from fork

https://github.com/clidey/whodbAnguelJan 27, 2025via ghsa
11 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

News mentions

0

No linked articles in our index yet.