diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs
index fe28c346..3838db99 100644
--- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlDriverProbe.cs
@@ -19,7 +19,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
/// timeout, a cancellation — becomes a red , so the AdminUI has nothing to
/// catch.
/// The resolved connection string never reaches the result message. A red result names the
-/// failure class, never the string — the same credential discipline the factory and the driver keep.
+/// failure class, never the string — the same credential discipline the factory and the driver keep.
+/// That includes the JSON parser: a malformed config is reported as a class, with the parser's own
+/// message withheld exactly as SqlDriverBrowser.ParseDocument withholds it, because the malformed
+/// text may itself be a pasted connection string.
///
public sealed class SqlDriverProbe : IDriverProbe
{
@@ -80,7 +83,16 @@ public sealed class SqlDriverProbe : IDriverProbe
}
catch (Exception ex) when (ex is System.Text.Json.JsonException or ArgumentNullException)
{
- return new DriverProbeResult(false, $"Config JSON is invalid: {ex.Message}", null);
+ // The parser's message is WITHHELD, matching SqlDriverBrowser.ParseDocument. Nothing has been
+ // extracted at this point, so there is no known secret to redact the message against — and the
+ // blob both types read is the one the AdminUI Sql picker composes, which carries a top-level
+ // `connectionString` literal. A malformed paste is therefore exactly the text that reaches here,
+ // and System.Text.Json quotes the offending token and states the JSON path and byte position.
+ return new DriverProbeResult(
+ false,
+ "Config JSON is not valid JSON. (The parser's message is withheld because the malformed "
+ + "text may carry a connection string.)",
+ null);
}
if (dto is null)
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverProbeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverProbeTests.cs
index e8eb97a7..54235cee 100644
--- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverProbeTests.cs
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/SqlDriverProbeTests.cs
@@ -79,6 +79,38 @@ public sealed class SqlDriverProbeTests
result.Message.ShouldNotBeNullOrWhiteSpace();
}
+ ///
+ /// A malformed config is reported as a failure CLASS, never as the parser's own message — the same
+ /// discipline SqlDriverBrowser.ParseDocument keeps, and for the same reason: the blob the two
+ /// read is the one the AdminUI Sql picker composes, which carries a top-level connectionString
+ /// literal, so a malformed paste is exactly the text that reaches here.
+ /// 's message quotes the offending token and states the JSON path
+ /// and the byte position. None of it may leave the probe: at this point nothing has been extracted,
+ /// so there is no known secret to redact against.
+ ///
+ [Fact]
+ public async Task Probe_malformedConfig_withholdsTheParserMessage()
+ {
+ using var fixture = new SqlitePollFixture();
+ var probe = SqlDriverProbe.ForTest(fixture.Factory, new SqliteDialect());
+
+ // A pasted literal whose password carries an unquoted ';' — mis-quoted exactly the way an operator's
+ // paste is, so the parse fails partway THROUGH the credential.
+ const string malformed =
+ """{"provider":"SqlServer","connectionString":Password=Sup3r;SecretTail}""";
+
+ var result = await probe.ProbeAsync(malformed, TimeSpan.FromSeconds(5), CancellationToken.None);
+
+ result.Ok.ShouldBeFalse();
+ result.Message.ShouldNotBeNull();
+ result.Message!.ShouldNotContain("LineNumber");
+ result.Message.ShouldNotContain("BytePositionInLine");
+ result.Message.ShouldNotContain("Sup3r");
+ result.Message.ShouldNotContain("SecretTail");
+ // Still actionable: it names what is wrong and why the detail is missing.
+ result.Message.ShouldContain("not valid JSON");
+ }
+
[Fact]
public async Task Probe_providerNumberSpelling_parses_soTestConnectMatchesDeploy()
{