diff --git a/src/SynoSharp/Ssh/SynologySshOptions.cs b/src/SynoSharp/Ssh/SynologySshOptions.cs
index f0c2dfb..aa69536 100644
--- a/src/SynoSharp/Ssh/SynologySshOptions.cs
+++ b/src/SynoSharp/Ssh/SynologySshOptions.cs
@@ -60,4 +60,14 @@ public sealed record SynologySshOptions
PrivateKeyPath = Environment.GetEnvironmentVariable("SYNOLOGY_SSH_KEY"),
};
}
+
+ ///
+ /// Redacted representation. The synthesized record ToString() would
+ /// otherwise print (the login + sudo password),
+ /// leaking it into any log or interpolated string. The password is never emitted.
+ ///
+ public override string ToString() =>
+ $"SynologySshOptions {{ Host = {Host}, Port = {Port}, Username = {Username}, " +
+ $"Password = {(Password is null ? "null" : "***")}, PrivateKeyPath = {PrivateKeyPath}, " +
+ $"AcceptAnyHostKey = {AcceptAnyHostKey} }}";
}
diff --git a/src/SynoSharp/SynologyApiClient.cs b/src/SynoSharp/SynologyApiClient.cs
index d6fbe30..244823d 100644
--- a/src/SynoSharp/SynologyApiClient.cs
+++ b/src/SynoSharp/SynologyApiClient.cs
@@ -47,14 +47,33 @@ public SynologyApiClient(SynologyOptions options, HttpClient? httpClient = null)
: new Uri(options.BaseUrl.AbsoluteUri + "/");
}
- /// Authenticate (SYNO.API.Auth) and cache the session id.
+ ///
+ /// Authenticate (SYNO.API.Auth) and cache the session id.
+ ///
+ /// Credentials are sent as a POST form body, not in the query string,
+ /// so the account and password never land in DSM access logs, proxy logs, or
+ /// HttpClient request-URI logging. DSM 7.x accepts the auth parameters
+ /// as form fields on auth.cgi.
+ ///
+ ///
public async Task LoginAsync(CancellationToken cancellationToken = default)
{
- var query = $"webapi/auth.cgi?api=SYNO.API.Auth&version=3&method=login" +
- $"&account={Uri.EscapeDataString(_options.Username)}" +
- $"&passwd={Uri.EscapeDataString(_options.Password)}&session=DSM&format=sid";
+ using var form = new FormUrlEncodedContent(new Dictionary
+ {
+ ["api"] = "SYNO.API.Auth",
+ ["version"] = "3",
+ ["method"] = "login",
+ ["account"] = _options.Username,
+ ["passwd"] = _options.Password,
+ ["session"] = "DSM",
+ ["format"] = "sid",
+ });
- var envelope = await _http.GetFromJsonAsync(query, cancellationToken).ConfigureAwait(false);
+ using var response = await _http.PostAsync("webapi/auth.cgi", form, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ var envelope = await response.Content
+ .ReadFromJsonAsync(cancellationToken).ConfigureAwait(false);
if (!envelope.TryGetProperty("success", out var ok) || !ok.GetBoolean())
{
throw new InvalidOperationException("Synology login failed (SYNO.API.Auth).");
diff --git a/src/SynoSharp/SynologyOptions.cs b/src/SynoSharp/SynologyOptions.cs
index 7a47704..381e4ff 100644
--- a/src/SynoSharp/SynologyOptions.cs
+++ b/src/SynoSharp/SynologyOptions.cs
@@ -34,4 +34,12 @@ public sealed record SynologyOptions
return new SynologyOptions { BaseUrl = new Uri(baseUrl), Username = user, Password = pass, VerifyTls = verifyTls };
}
+
+ ///
+ /// Redacted representation. The synthesized record ToString() would
+ /// otherwise print , leaking it into any log or
+ /// interpolated string. The password is never emitted.
+ ///
+ public override string ToString() =>
+ $"SynologyOptions {{ BaseUrl = {BaseUrl}, Username = {Username}, Password = ***, VerifyTls = {VerifyTls} }}";
}
diff --git a/tests/SynoSharp.Tests/SynologyTests.cs b/tests/SynoSharp.Tests/SynologyTests.cs
index 31e0a7c..5f6c38e 100644
--- a/tests/SynoSharp.Tests/SynologyTests.cs
+++ b/tests/SynoSharp.Tests/SynologyTests.cs
@@ -16,6 +16,22 @@ public void VerifyTls_defaults_to_true()
Assert.True(options.VerifyTls);
}
+
+ [Fact]
+ public void ToString_does_not_leak_the_password()
+ {
+ var options = new SynologyOptions
+ {
+ BaseUrl = new Uri("https://nas:5001"),
+ Username = "u",
+ Password = "super-secret-password",
+ };
+
+ var text = options.ToString();
+
+ Assert.DoesNotContain("super-secret-password", text);
+ Assert.Contains("***", text);
+ }
}
///