mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: handle unescaped userinfo in postgres url (#11396)
* fix: handle unescaped userinfo in postgres url * add tests * fix tests
This commit is contained in:
+45
-2
@@ -648,7 +648,12 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
options.Database = dbmem.New()
|
||||
options.Pubsub = pubsub.NewInMemory()
|
||||
} else {
|
||||
sqlDB, err := ConnectToPostgres(ctx, logger, sqlDriver, vals.PostgresURL.String())
|
||||
dbURL, err := escapePostgresURLUserInfo(vals.PostgresURL.String())
|
||||
if err != nil {
|
||||
return xerrors.Errorf("escaping postgres URL: %w", err)
|
||||
}
|
||||
|
||||
sqlDB, err := ConnectToPostgres(ctx, logger, sqlDriver, dbURL)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("connect to postgres: %w", err)
|
||||
}
|
||||
@@ -657,7 +662,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
}()
|
||||
|
||||
options.Database = database.New(sqlDB)
|
||||
options.Pubsub, err = pubsub.New(ctx, sqlDB, vals.PostgresURL.String())
|
||||
options.Pubsub, err = pubsub.New(ctx, sqlDB, dbURL)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create pubsub: %w", err)
|
||||
}
|
||||
@@ -2433,3 +2438,41 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// If the user provides a postgres URL with a password that contains special
|
||||
// characters, the URL will be invalid. We need to escape the password so that
|
||||
// the URL parse doesn't fail at the DB connector level.
|
||||
func escapePostgresURLUserInfo(v string) (string, error) {
|
||||
_, err := url.Parse(v)
|
||||
// I wish I could use errors.Is here, but this error is not declared as a
|
||||
// variable in net/url. :(
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "net/url: invalid userinfo") {
|
||||
// If the URL is invalid, we assume it is because the password contains
|
||||
// special characters that need to be escaped.
|
||||
|
||||
// get everything before first @
|
||||
parts := strings.SplitN(v, "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", xerrors.Errorf("invalid postgres url with userinfo: %s", v)
|
||||
}
|
||||
start := parts[0]
|
||||
// get password, which is the last item in start when split by :
|
||||
startParts := strings.Split(start, ":")
|
||||
password := startParts[len(startParts)-1]
|
||||
// escape password, and replace the last item in the startParts slice
|
||||
// with the escaped password.
|
||||
//
|
||||
// url.PathEscape is used here because url.QueryEscape
|
||||
// will not escape spaces correctly.
|
||||
newPassword := url.PathEscape(password)
|
||||
startParts[len(startParts)-1] = newPassword
|
||||
start = strings.Join(startParts, ":")
|
||||
return start + "@" + parts[1], nil
|
||||
}
|
||||
|
||||
return "", xerrors.Errorf("parse postgres url: %w", err)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
@@ -296,3 +297,53 @@ func TestIsDERPPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapePostgresURLUserInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testcases := []struct {
|
||||
input string
|
||||
output string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
input: "postgres://coder:coder@localhost:5432/coder",
|
||||
output: "postgres://coder:coder@localhost:5432/coder",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "postgres://coder:co{der@localhost:5432/coder",
|
||||
output: "postgres://coder:co%7Bder@localhost:5432/coder",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "postgres://coder:co:der@localhost:5432/coder",
|
||||
output: "postgres://coder:co:der@localhost:5432/coder",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "postgres://coder:co der@localhost:5432/coder",
|
||||
output: "postgres://coder:co%20der@localhost:5432/coder",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
input: "postgres://local host:5432/coder",
|
||||
output: "",
|
||||
err: xerrors.New("parse postgres url: parse \"postgres://local host:5432/coder\": invalid character \" \" in host name"),
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
tc := tc
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
o, err := escapePostgresURLUserInfo(tc.input)
|
||||
require.Equal(t, tc.output, o)
|
||||
if tc.err != nil {
|
||||
require.Error(t, err)
|
||||
require.EqualValues(t, tc.err.Error(), err.Error())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user