Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions exporter/pbm_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"go.mongodb.org/mongo-driver/mongo"

"github.com/percona/mongodb_exporter/internal/proto"
"github.com/percona/mongodb_exporter/internal/redact"
"github.com/percona/mongodb_exporter/internal/util"
)

Expand Down Expand Up @@ -87,7 +88,7 @@ func (p *pbmCollector) collect(ch chan<- prometheus.Metric) {
pbmEnabledMetric := 0
pbmClient, err := sdk.NewClient(p.ctx, p.mongoURI)
if err != nil {
logger.Warn("failed to create PBM client", "error", err.Error())
logger.Warn("failed to create PBM client", "error", redact.Error(err))
return
}
defer func() {
Expand Down Expand Up @@ -136,7 +137,8 @@ func (p *pbmCollector) collect(ch chan<- prometheus.Metric) {
func (p *pbmCollector) pbmAgentMetrics(ctx context.Context, pbmClient *sdk.Client, l *slog.Logger, currentNode *proto.HelloResponse) []prometheus.Metric {
clusterStatus, err := cli.ClusterStatus(ctx, pbmClient, cli.RSConfGetter(p.mongoURI))
if err != nil {
l.Error("failed to get cluster status", "error", err.Error())
// PBM wraps the URI it was handed into this error, so it cannot be logged as it comes.
l.Error("failed to get cluster status", "error", redact.Error(err))
return nil
}

Expand Down
10 changes: 7 additions & 3 deletions exporter/seedlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,27 @@ import (
"net/url"
"strconv"
"strings"

"github.com/percona/mongodb_exporter/internal/redact"
)

// GetSeedListFromSRV converts mongodb+srv URI to flat connection string.
func GetSeedListFromSRV(uri string, logger *slog.Logger) string { //nolint:cyclop
uriParsed, err := url.Parse(uri)
if err != nil {
log.Fatalf("Failed to parse URI %s: %v", uri, err)
// The parse error is not logged: it quotes the URI cut short at the byte url.Parse choked
// on, and when that byte is "?" or "#" from the password, the quote keeps the part before.
log.Fatalf("Failed to parse URI %s", redact.MongoURI(uri))
}

cname, srvRecords, err := net.LookupSRV("mongodb", "tcp", uriParsed.Hostname())
if err != nil {
logger.Error("Failed to lookup SRV records", "uri", uri, "error", err)
logger.Error("Failed to lookup SRV records", "uri", redact.MongoURI(uri), "error", err)
return uri
}

if len(srvRecords) == 0 {
logger.Error("No SRV records found", "uri", uri)
logger.Error("No SRV records found", "uri", redact.MongoURI(uri))
return uri
}

Expand Down
7 changes: 6 additions & 1 deletion exporter/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/exporter-toolkit/web"

"github.com/percona/mongodb_exporter/internal/redact"
)

// ServerMap stores http handlers for each host
Expand Down Expand Up @@ -178,7 +180,10 @@ func buildServerMap(exporters []*Exporter, log *slog.Logger) ServerMap {
if parsedURL, err := url.Parse(e.opts.URI); err == nil {
servers[parsedURL.Host] = e.Handler()
} else {
log.Error("Unable to parse provided address as url", "address", e.opts.URI, "error", err)
// The parse error is not logged: it quotes the URI cut short at the byte url.Parse
// choked on, and when that byte is "?" or "#" from the password, the quote keeps the
// part before.
log.Error("Unable to parse provided address as url", "address", redact.MongoURI(e.opts.URI))
}
}

Expand Down
59 changes: 59 additions & 0 deletions internal/redact/redact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// mongodb_exporter
// Copyright (C) 2025 Percona LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package redact hides the credentials embedded in MongoDB connection URIs before they are logged.
package redact

import (
"net/url"
"regexp"
)

// credentialsRE matches the userinfo of a MongoDB connection URI that carries a password. It is
// used when url.Parse is of no help: either the URI is malformed, or it is quoted inside a longer
// string such as an error message.
//
// The password group is deliberately permissive and greedy. A password may legally contain any of
// "/", "?", "#", "@" and space, none of which survive url.Parse unescaped, so the malformed URIs
// this expression exists to handle are exactly the ones that carry them; excluding those bytes
// would silently pass the password through. Being greedy anchors the match on the last "@", which
// is what url.Parse itself treats as the end of the userinfo. The cost is over-redaction when a
// credential-free URI happens to contain a later "@" — harmless next to leaking a password.
var credentialsRE = regexp.MustCompile(`(mongodb(?:\+srv)?://)([^:@]*):(.*)@`)

// replacement keeps the scheme and the user name and drops the password. It matches what
// (*url.URL).Redacted does.
const replacement = "${1}${2}:xxxxx@"

// MongoURI returns uri with its password replaced by a placeholder, so that it is safe to log.
func MongoURI(uri string) string {
parsed, err := url.Parse(uri)
if err == nil && parsed.User != nil {
return parsed.Redacted()
}

return credentialsRE.ReplaceAllString(uri, replacement)
}

// Error returns the message of err with the password of any MongoDB URI it quotes replaced by a
// placeholder. url.Parse and the MongoDB driver embed the offending URI in their error messages,
// so logging the error alone is enough to leak the credentials.
func Error(err error) string {
if err == nil {
return ""
}

return credentialsRE.ReplaceAllString(err.Error(), replacement)
}
Loading
Loading