fix: handle unescaped userinfo in postgres url (#11396)

* fix: handle unescaped userinfo in postgres url

* add tests

* fix tests
This commit is contained in:
Garrett Delfosse
2024-01-04 08:46:00 -05:00
committed by GitHub
parent f0db302df2
commit 5981abd689
2 changed files with 96 additions and 2 deletions
+45 -2
View File
@@ -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
}
+51
View File
@@ -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)
}
})
}
}