security: hardening по результатам аудита безопасности
This commit is contained in:
@@ -6,11 +6,21 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/pgdump"
|
||||
)
|
||||
|
||||
var (
|
||||
passwordPattern = regexp.MustCompile(`(?i)\bpassword=[^\s]*`)
|
||||
hostPattern = regexp.MustCompile(`(?i)\bhost=[^\s]*`)
|
||||
// excludeTablePattern accepts unquoted PostgreSQL identifiers or the
|
||||
// schema.table form. It rejects shell-special characters and injection
|
||||
// payloads while still allowing the default Synapse table name.
|
||||
excludeTablePattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_$]*(\.[a-zA-Z_][a-zA-Z0-9_$]*)?$`)
|
||||
)
|
||||
|
||||
// adapter provides pg_dump functionality using the system's pg_dump binary.
|
||||
type adapter struct {
|
||||
commandContext func(ctx context.Context, name string, arg ...string) *exec.Cmd
|
||||
@@ -24,6 +34,10 @@ func New() pgdump.Dumper {
|
||||
}
|
||||
|
||||
// Dump executes pg_dump and writes the output to sink.
|
||||
//
|
||||
// The password is never passed through the PGPASSWORD environment variable;
|
||||
// instead a temporary .pgpass file with 0o600 permissions is created and
|
||||
// pointed to via PGPASSFILE.
|
||||
func (a *adapter) Dump(
|
||||
ctx context.Context,
|
||||
opts pgdump.Options,
|
||||
@@ -39,14 +53,24 @@ func (a *adapter) Dump(
|
||||
}
|
||||
}()
|
||||
|
||||
args := buildArgs(opts)
|
||||
args, err := buildArgs(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := a.commandContext(ctx, "pg_dump", args...)
|
||||
|
||||
env := os.Environ()
|
||||
if opts.Password != "" {
|
||||
env = append(env, fmt.Sprintf("PGPASSWORD=%s", opts.Password))
|
||||
|
||||
pgpassPath, cleanupPgpass, err := writePgPassFile(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanupPgpass()
|
||||
if pgpassPath != "" {
|
||||
env = append(env, fmt.Sprintf("PGPASSFILE=%s", pgpassPath))
|
||||
}
|
||||
|
||||
if opts.Host != "" {
|
||||
env = append(env, fmt.Sprintf("PGHOST=%s", opts.Host))
|
||||
}
|
||||
@@ -66,7 +90,7 @@ func (a *adapter) Dump(
|
||||
cmd.Stdout = sink
|
||||
|
||||
runErr := cmd.Run()
|
||||
stderr := strings.TrimSpace(stderrBuilder.String())
|
||||
stderr := sanitizeStderr(strings.TrimSpace(stderrBuilder.String()))
|
||||
|
||||
if runErr != nil {
|
||||
if exitErr, ok := runErr.(*exec.ExitError); ok {
|
||||
@@ -82,7 +106,56 @@ func (a *adapter) Dump(
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildArgs(opts pgdump.Options) []string {
|
||||
// writePgPassFile creates a temporary .pgpass file when a password is provided.
|
||||
// The returned cleanup function removes the file; callers should defer it.
|
||||
func writePgPassFile(opts pgdump.Options) (string, func(), error) {
|
||||
if opts.Password == "" {
|
||||
return "", func() {}, nil
|
||||
}
|
||||
|
||||
passFile, err := os.CreateTemp("", "pgpass-*.conf")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("create temporary pgpass file: %w", err)
|
||||
}
|
||||
path := passFile.Name()
|
||||
cleanup := func() { _ = os.Remove(path) }
|
||||
|
||||
line := fmt.Sprintf(
|
||||
"%s:%d:%s:%s:%s\n",
|
||||
opts.Host,
|
||||
opts.Port,
|
||||
opts.Database,
|
||||
opts.User,
|
||||
opts.Password,
|
||||
)
|
||||
if _, err := passFile.WriteString(line); err != nil {
|
||||
_ = passFile.Close()
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("write temporary pgpass file: %w", err)
|
||||
}
|
||||
if err := passFile.Close(); err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("close temporary pgpass file: %w", err)
|
||||
}
|
||||
if err := os.Chmod(path, 0o600); err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("chmod temporary pgpass file: %w", err)
|
||||
}
|
||||
return path, cleanup, nil
|
||||
}
|
||||
|
||||
// sanitizeStderr removes sensitive connection-string fragments from pg_dump
|
||||
// diagnostics before they are logged.
|
||||
func sanitizeStderr(input string) string {
|
||||
if input == "" {
|
||||
return ""
|
||||
}
|
||||
out := passwordPattern.ReplaceAllString(input, "password=***")
|
||||
out = hostPattern.ReplaceAllString(out, "host=***")
|
||||
return out
|
||||
}
|
||||
|
||||
func buildArgs(opts pgdump.Options) ([]string, error) {
|
||||
excludeTables := opts.ExcludeTables
|
||||
if len(excludeTables) == 0 {
|
||||
excludeTables = make([]string, 1)
|
||||
@@ -93,8 +166,11 @@ func buildArgs(opts pgdump.Options) []string {
|
||||
args = append(args, "--format=custom")
|
||||
|
||||
for _, table := range excludeTables {
|
||||
if !excludeTablePattern.MatchString(table) {
|
||||
return nil, fmt.Errorf("invalid exclude-table identifier %q", table)
|
||||
}
|
||||
args = append(args, "--exclude-table="+table)
|
||||
}
|
||||
|
||||
return args
|
||||
return args, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user