From 464b66a7632a59e671398c567839ea6117e29d9d Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 27 Dec 2025 09:13:34 +0100 Subject: [PATCH] Do not include password in data source name with Persist Security Info=true Closes #6394 --- src/Npgsql/NpgsqlDataSource.cs | 26 ++++++++++++++++---------- test/Npgsql.Tests/MetricTests.cs | 25 +++++++++++++++++++++++++ test/Npgsql.Tests/TracingTests.cs | 21 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/Npgsql/NpgsqlDataSource.cs b/src/Npgsql/NpgsqlDataSource.cs index 280b32c128..dabbc978a5 100644 --- a/src/Npgsql/NpgsqlDataSource.cs +++ b/src/Npgsql/NpgsqlDataSource.cs @@ -94,15 +94,8 @@ private protected readonly Dictionary> _pendi readonly INpgsqlNameTranslator _defaultNameTranslator; readonly IDisposable? _eventSourceEvents; - internal NpgsqlDataSource( - NpgsqlConnectionStringBuilder settings, - NpgsqlDataSourceConfiguration dataSourceConfig, bool reportMetrics) + internal NpgsqlDataSource(NpgsqlConnectionStringBuilder settings, NpgsqlDataSourceConfiguration dataSourceConfig, bool reportMetrics) { - Settings = settings; - ConnectionString = settings.PersistSecurityInfo - ? settings.ToString() - : settings.ToStringWithoutPassword(); - Configuration = dataSourceConfig; (var name, @@ -128,6 +121,21 @@ internal NpgsqlDataSource( Debug.Assert(_passwordProvider is null || _passwordProviderAsync is not null); + Settings = settings; + + if (settings.PersistSecurityInfo) + { + ConnectionString = settings.ToString(); + + // The data source name is reported in tracing/metrics, so avoid leaking the password through there. + Name = name ?? settings.ToStringWithoutPassword(); + } + else + { + ConnectionString = settings.ToStringWithoutPassword(); + Name = name ?? ConnectionString; + } + _password = settings.Password; if (_periodicPasswordSuccessRefreshInterval != default) @@ -144,8 +152,6 @@ internal NpgsqlDataSource( _passwordRefreshTask = Task.Run(RefreshPassword); } - Name = name ?? ConnectionString; - // TODO this needs a rework, but for now we just avoid tracking multi-host data sources directly. if (reportMetrics) { diff --git a/test/Npgsql.Tests/MetricTests.cs b/test/Npgsql.Tests/MetricTests.cs index 235f8b4e27..9a8b2757e3 100644 --- a/test/Npgsql.Tests/MetricTests.cs +++ b/test/Npgsql.Tests/MetricTests.cs @@ -127,6 +127,31 @@ public async Task ConnectionMax() Assert.That(tags["db.client.connection.pool.name"], Is.EqualTo(dataSource.Name)); } + [Test] + public async Task Password_does_not_leak_via_datasource_name([Values] bool persistSecurityInfo) + { + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("Npgsql") + .AddInMemoryExporter(exportedItems) + .Build(); + + var dataSourceBuilder = base.CreateDataSourceBuilder(); + dataSourceBuilder.ConnectionStringBuilder.ApplicationName = "MetricsDataSource" + Interlocked.Increment(ref _dataSourceCounter); + dataSourceBuilder.ConnectionStringBuilder.PersistSecurityInfo = persistSecurityInfo; + // Do not set the data source name - this makes it default to the connection string, but without + // the password (even when Persist Security Info is true) + await using var dataSource = dataSourceBuilder.Build(); + + meterProvider.ForceFlush(); + + var metric = exportedItems.Single(m => m.Name == "db.client.connection.max"); + var point = GetFilteredPoints(metric.GetMetricPoints(), dataSource.Name).First(); + var tags = ToDictionary(point.Tags); + var connectionString = new NpgsqlConnectionStringBuilder((string)tags["db.client.connection.pool.name"]!); + Assert.That(connectionString.Password, Is.Null); + } + static Dictionary ToDictionary(ReadOnlyTagCollection tags) { var dict = new Dictionary(); diff --git a/test/Npgsql.Tests/TracingTests.cs b/test/Npgsql.Tests/TracingTests.cs index 1033dc7a55..f845861366 100644 --- a/test/Npgsql.Tests/TracingTests.cs +++ b/test/Npgsql.Tests/TracingTests.cs @@ -811,6 +811,27 @@ public async Task Copy_ConfigureTracing() Assert.That(tags["custom_tag"], Is.EqualTo("custom_value")); } + [Test] + public async Task Password_does_not_leak_via_datasource_name([Values] bool persistSecurityInfo) + { + var dataSourceBuilder = CreateDataSourceBuilder(); + dataSourceBuilder.ConnectionStringBuilder.PersistSecurityInfo = persistSecurityInfo; + // Do not set the data source name - this makes it default to the connection string, but without + // the password (even when Persist Security Info is true) + await using var dataSource = dataSourceBuilder.Build(); + await using var connection = await dataSource.OpenConnectionAsync(); + + using var activityListener = StartListener(out var activities); + + await ExecuteScalar(connection, async, isBatch: false, query: "SELECT 42"); + + var activity = GetSingleActivity(activities, "postgresql", "postgresql"); + + var tags = activity.TagObjects.ToDictionary(t => t.Key, t => t.Value); + var connectionString = new NpgsqlConnectionStringBuilder((string)tags["db.npgsql.data_source"]!); + Assert.That(connectionString.Password, Is.Null); + } + static ActivityListener StartListener(out List activities) { var a = new List();