CVE-2022-3023 (Go, published 2022-11-04T12:15Z)
Description: value: Use of Externally-Controlled Format String in GitHub repository pingcap/tidb prior to 6.4.0, 6.1.3.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-134 \u2014 Use of Externally-Controlled Format String
The product uses a function that accepts a format string as an argument, but the format string originates from an external source.
Repository: https://github.com/pingcap/tidb
Files changed:
br/pkg/lightning/checkpoints/checkpoints.go
br/pkg/lightning/common/util.go
br/pkg/lightning/common/util_test.go
br/pkg/lightning/config/config.go
br/pkg/lightning/config/config_test.go
cmd/importer/db.go
dumpling/export/config.go
dumpling/export/dump.go
dumpling/export/sql.go
dumpling/export/sql_test.go
dumpling/tests/s3/import.go
util/dbutil/common.go
Commit message: *: don't use DSN to avoid some security problems (#38342)

 

Vulnerable code:
		db, err := common.ConnectMySQL(cfg.Checkpoint.DSN)
		db, err := sql.Open("mysql", cfg.Checkpoint.DSN)
	"net/url"
func (param *MySQLConnectParam) ToDSN() string {
	hostPort := net.JoinHostPort(param.Host, strconv.Itoa(param.Port))
	dsn := fmt.Sprintf("%s:%s@tcp(%s)/?charset=utf8mb4&sql_mode='%s'&maxAllowedPacket=%d&tls=%s",
		param.User, param.Password, hostPort,
		param.SQLMode, param.MaxAllowedPacket, param.TLS)
		dsn += fmt.Sprintf("&%s='%s'", k, url.QueryEscape(v))

	return dsn
func tryConnectMySQL(dsn string) (*sql.DB, error) {
	driverName := "mysql"
	failpoint.Inject("MockMySQLDriver", func(val failpoint.Value) {
		driverName = val.(string)
	db, err := sql.Open(driverName, dsn)
func ConnectMySQL(dsn string) (*sql.DB, error) {
	cfg, err := mysql.ParseDSN(dsn)
	if err != nil {
		return nil, errors.Trace(err)
	}
	db, firstErr := tryConnectMySQL(dsn)
			db, err = tryConnectMySQL(cfg.FormatDSN())
				return db, nil
	db, err := ConnectMySQL(param.ToDSN())
	"database/sql"
	"database/sql/driver"
	"math/rand"
	"strconv"
	tmysql "github.com/pingcap/tidb/errno"
func TestToDSN(t *testing.T) {
	param := common.MySQLConnectParam{
		Host:             "127.0.0.1",
		Port:             4000,
		User:             "root",
		Password:         "123456",
		SQLMode:          "strict",
		MaxAllowedPacket: 1234,
		TLS:              "cluster",
		Vars: map[string]string{
			"tidb_distsql_scan_concurrency": "1",
		},
	}
	require.Equal(t, "root:123456@tcp(127.0.0.1:4000)/?charset=utf8mb4&sql_mode='strict'&maxAllowedPacket=1234&tls=cluster&tidb_distsql_scan_concurrency='1'", param.ToDSN())

	param.Host = "::1"
	require.Equal(t, "root:123456@tcp([::1]:4000)/?charset=utf8mb4&sql_mode='strict'&maxAllowedPacket=1234&tls=cluster&tidb_distsql_scan_concurrency='1'", param.ToDSN())
}

type mockDriver struct {
	driver.Driver
	plainPsw string
}

func (m *mockDriver) Open(dsn string) (driver.Conn, error) {
	cfg, err := mysql.ParseDSN(dsn)
	if err != nil {
		return nil, err
	}
	accessDenied := cfg.Passwd != m.plainPsw
	return &mockConn{accessDenied: accessDenied}, nil
}

type mockConn struct {
	driver.Conn
	driver.Pinger
	accessDenied bool
}

func (c *mockConn) Ping(ctx context.Context) error {
	if c.accessDenied {
		return &mysql.MySQLError{Number: tmysql.ErrAccessDenied, Message: "access denied"}
	}
	return nil
}

func (c *mockConn) Close() error {
	return nil
}

	driverName := "mysql-mock-" + strconv.Itoa(rand.Int())
	sql.Register(driverName, &mockDriver{plainPsw: plainPsw})
		"github.com/pingcap/tidb/br/pkg/lightning/common/MockMySQLDriver",
		fmt.Sprintf("return(\"%s\")", driverName)))
		require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/br/pkg/lightning/common/MockMySQLDriver"))
	db, err := param.Connect()
	require.NoError(t, db.Close())
	db, err = param.Connect()
	require.NoError(t, db.Close())
	Schema           string                 `toml:"schema" json:"schema"`
	DSN              string                 `toml:"dsn" json:"-"` // DSN may contain password, don't expose this to JSON.
	Driver           string                 `toml:"driver" json:"driver"`
	Enable           bool                   `toml:"enable" json:"enable"`
	KeepAfterSuccess CheckpointKeepStrategy `toml:"keep-after-success" json:"keep-after-success"`
			cfg.Checkpoint.DSN = param.ToDSN()
	"github.com/pingcap/tidb/parser/mysql"
	require.Equal(t, "guest:12345@tcp(172.16.30.11:4001)/?charset=utf8mb4&sql_mode='"+mysql.DefaultSQLMode+"'&maxAllowedPacket=67108864&tls=false", taskCfg.Checkpoint.DSN)
	_ "github.com/go-sql-driver/mysql"
	dbDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Name)
	db, err := sql.Open("mysql", dbDSN)

	return db, nil
var openDBFunc = sql.Open
	pool, err := sql.Open("mysql", conf.GetDSN(""))
	d.dbHandle = pool
	if d.dbHandle, err = resetDBWithSessionParams(d.tctx, pool, conf.GetDSN(""), conf.SessionParams); err != nil {
	dbHandle, err := openDBFunc("mysql", conf.GetDSN(""))
	"net/url"
func resetDBWithSessionParams(tctx *tcontext.Context, db *sql.DB, dsn string, params map[string]interface{}) (*sql.DB, error) {
		dsn += fmt.Sprintf("&%s=%s", k, url.QueryEscape(s))
	newDB, err := sql.Open("mysql", dsn)
	if err == nil {
		// ping to make sure all session parameters are set correctly
		err = newDB.PingContext(tctx)
		if err != nil {
			newDB.Close()
		}
	return newDB, errors.Trace(err)
	openDBFunc = func(_, _ string) (*sql.DB, error) {
		dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4", "root", "", "127.0.0.1", port, database)
	"net/url"
	var dbDSN string
		dbDSN = fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8mb4&tidb_snapshot=%s", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Snapshot)
	} else {
		dbDSN = fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8mb4", cfg.User, cfg.Password, cfg.Host, cfg.Port)
		dbDSN += fmt.Sprintf("&%s=%%27%s%%27", key, url.QueryEscape(val))
	dbConn, err := sql.Open("mysql", dbDSN)

	err = dbConn.Ping()
	return dbConn, errors.Trace(err)

Fixed code:
		var (
			db  *sql.DB
			err error
		)
		if cfg.Checkpoint.MySQLParam != nil {
			db, err = cfg.Checkpoint.MySQLParam.Connect()
		} else {
			db, err = sql.Open("mysql", cfg.Checkpoint.DSN)
		}
		var (
			db  *sql.DB
			err error
		)
		if cfg.Checkpoint.MySQLParam != nil {
			db, err = cfg.Checkpoint.MySQLParam.Connect()
		} else {
			db, err = sql.Open("mysql", cfg.Checkpoint.DSN)
		}
func (param *MySQLConnectParam) ToDriverConfig() *mysql.Config {
	cfg := mysql.NewConfig()
	cfg.Params = make(map[string]string)

	cfg.User = param.User
	cfg.Passwd = param.Password
	cfg.Net = "tcp"
	cfg.Addr = net.JoinHostPort(param.Host, strconv.Itoa(param.Port))
	cfg.Params["charset"] = "utf8mb4"
	cfg.Params["sql_mode"] = fmt.Sprintf("'%s'", param.SQLMode)
	cfg.MaxAllowedPacket = int(param.MaxAllowedPacket)
	cfg.TLSConfig = param.TLS
		cfg.Params[k] = fmt.Sprintf("'%s'", v)
	return cfg
func tryConnectMySQL(cfg *mysql.Config) (*sql.DB, error) {
	failpoint.Inject("MustMySQLPassword", func(val failpoint.Value) {
		pwd := val.(string)
		if cfg.Passwd != pwd {
			failpoint.Return(nil, &mysql.MySQLError{Number: tmysql.ErrAccessDenied, Message: "access denied"})
		}
		failpoint.Return(nil, nil)
	c, err := mysql.NewConnector(cfg)
	db := sql.OpenDB(c)
func ConnectMySQL(cfg *mysql.Config) (*sql.DB, error) {
	db, firstErr := tryConnectMySQL(cfg)
			db2, err := tryConnectMySQL(cfg)
				return db2, nil
	db, err := ConnectMySQL(param.ToDriverConfig())
		"github.com/pingcap/tidb/br/pkg/lightning/common/MustMySQLPassword",
		fmt.Sprintf("return(\"%s\")", plainPsw)))
		require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/br/pkg/lightning/common/MustMySQLPassword"))
	_, err := param.Connect()
	_, err = param.Connect()
	Schema           string                    `toml:"schema" json:"schema"`
	DSN              string                    `toml:"dsn" json:"-"` // DSN may contain password, don't expose this to JSON.
	MySQLParam       *common.MySQLConnectParam `toml:"-" json:"-"`   // For some security reason, we use MySQLParam instead of DSN.
	Driver           string                    `toml:"driver" json:"driver"`
	Enable           bool                      `toml:"enable" json:"enable"`
	KeepAfterSuccess CheckpointKeepStrategy    `toml:"keep-after-success" json:"keep-after-success"`
			cfg.Checkpoint.MySQLParam = &param
	equivalentDSN := taskCfg.Checkpoint.MySQLParam.ToDriverConfig().FormatDSN()
	expectedDSN := "guest:12345@tcp(172.16.30.11:4001)/?tls=false&maxAllowedPacket=67108864&charset=utf8mb4&sql_mode=%27ONLY_FULL_GROUP_BY%2CSTRICT_TRANS_TABLES%2CNO_ZERO_IN_DATE%2CNO_ZERO_DATE%2CERROR_FOR_DIVISION_BY_ZERO%2CNO_AUTO_CREATE_USER%2CNO_ENGINE_SUBSTITUTION%27"
	require.Equal(t, expectedDSN, equivalentDSN)
	mysql2 "github.com/go-sql-driver/mysql"
	driverCfg := mysql2.NewConfig()
	driverCfg.User = cfg.User
	driverCfg.Passwd = cfg.Password
	driverCfg.Net = "tcp"
	driverCfg.Addr = cfg.Host + ":" + strconv.Itoa(cfg.Port)
	driverCfg.DBName = cfg.Name

	c, err := mysql2.NewConnector(driverCfg)
	return sql.OpenDB(c), nil
// GetDriverConfig returns the MySQL driver config from Config.
func (conf *Config) GetDriverConfig(db string) *mysql.Config {
	driverCfg := mysql.NewConfig()
	// maxAllowedPacket=0 can be used to automatically fetch the max_allowed_packet variable from server on every connection.
	// https://github.com/go-sql-driver/mysql#maxallowedpacket
	hostPort := net.JoinHostPort(conf.Host, strconv.Itoa(conf.Port))
	driverCfg.User = conf.User
	driverCfg.Passwd = conf.Password
	driverCfg.Net = "tcp"
	driverCfg.Addr = hostPort
	driverCfg.DBName = db
	driverCfg.Collation = "utf8mb4_general_ci"
	driverCfg.ReadTimeout = conf.ReadTimeout
	driverCfg.WriteTimeout = 30 * time.Second
	driverCfg.InterpolateParams = true
	driverCfg.MaxAllowedPacket = 0
	if conf.Security.DriveTLSName != "" {
		driverCfg.TLSConfig = conf.Security.DriveTLSName
	}
	if conf.AllowCleartextPasswords {
		driverCfg.AllowCleartextPasswords = true
	}
	return driverCfg
}

var openDBFunc = openDB
	c, err := mysql.NewConnector(conf.GetDriverConfig(""))
	d.dbHandle = sql.OpenDB(c)
	if d.dbHandle, err = resetDBWithSessionParams(d.tctx, pool, conf.GetDriverConfig(""), conf.SessionParams); err != nil {
func openDB(cfg *mysql.Config) (*sql.DB, error) {
	c, err := mysql.NewConnector(cfg)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return sql.OpenDB(c), nil
}

	dbHandle, err := openDBFunc(conf.GetDriverConfig(""))
func resetDBWithSessionParams(tctx *tcontext.Context, db *sql.DB, cfg *mysql.Config, params map[string]interface{}) (*sql.DB, error) {
	if cfg.Params == nil {
		cfg.Params = make(map[string]string)
	}

		cfg.Params[k] = s
	c, err := mysql.NewConnector(cfg)
	if err != nil {
		return nil, errors.Trace(err)
	}
	newDB := sql.OpenDB(c)
	// ping to make sure all session parameters are set correctly
	err = newDB.PingContext(tctx)
	if err != nil {
		newDB.Close()
	return newDB, nil
	openDBFunc = func(*mysql.Config) (*sql.DB, error) {
	"net"
	"strconv"
		dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4", "root", "", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), database)
	"net"
	driverCfg := mysql.NewConfig()
	driverCfg.Params = make(map[string]string)
	driverCfg.User = cfg.User
	driverCfg.Passwd = cfg.Password
	driverCfg.Net = "tcp"
	driverCfg.Addr = net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port))
	driverCfg.Params["charset"] = "utf8mb4"

		driverCfg.Params["tidb_snapshot"] = cfg.Snapshot
		driverCfg.Params[key] = fmt.Sprintf("'%s'", val)
	c, err := mysql.NewConnector(driverCfg)
	db := sql.OpenDB(c)
	err = db.Ping()
	return db, errors.Trace(err)