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
10 changes: 10 additions & 0 deletions src/SynoSharp/Ssh/SynologySshOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,14 @@ public sealed record SynologySshOptions
PrivateKeyPath = Environment.GetEnvironmentVariable("SYNOLOGY_SSH_KEY"),
};
}

/// <summary>
/// Redacted representation. The synthesized record <c>ToString()</c> would
/// otherwise print <see cref="Password"/> (the login + sudo password),
/// leaking it into any log or interpolated string. The password is never emitted.
/// </summary>
public override string ToString() =>
$"SynologySshOptions {{ Host = {Host}, Port = {Port}, Username = {Username}, " +
$"Password = {(Password is null ? "null" : "***")}, PrivateKeyPath = {PrivateKeyPath}, " +
$"AcceptAnyHostKey = {AcceptAnyHostKey} }}";
}
29 changes: 24 additions & 5 deletions src/SynoSharp/SynologyApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,33 @@ public SynologyApiClient(SynologyOptions options, HttpClient? httpClient = null)
: new Uri(options.BaseUrl.AbsoluteUri + "/");
}

/// <summary>Authenticate (<c>SYNO.API.Auth</c>) and cache the session id.</summary>
/// <summary>
/// Authenticate (<c>SYNO.API.Auth</c>) and cache the session id.
/// <para>
/// Credentials are sent as a <b>POST form body</b>, not in the query string,
/// so the account and password never land in DSM access logs, proxy logs, or
/// <c>HttpClient</c> request-URI logging. DSM 7.x accepts the auth parameters
/// as form fields on <c>auth.cgi</c>.
/// </para>
/// </summary>
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<string, string>
{
["api"] = "SYNO.API.Auth",
["version"] = "3",
["method"] = "login",
["account"] = _options.Username,
["passwd"] = _options.Password,
["session"] = "DSM",
["format"] = "sid",
});

var envelope = await _http.GetFromJsonAsync<JsonElement>(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<JsonElement>(cancellationToken).ConfigureAwait(false);
if (!envelope.TryGetProperty("success", out var ok) || !ok.GetBoolean())
{
throw new InvalidOperationException("Synology login failed (SYNO.API.Auth).");
Expand Down
8 changes: 8 additions & 0 deletions src/SynoSharp/SynologyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,12 @@ public sealed record SynologyOptions

return new SynologyOptions { BaseUrl = new Uri(baseUrl), Username = user, Password = pass, VerifyTls = verifyTls };
}

/// <summary>
/// Redacted representation. The synthesized record <c>ToString()</c> would
/// otherwise print <see cref="Password"/>, leaking it into any log or
/// interpolated string. The password is never emitted.
/// </summary>
public override string ToString() =>
$"SynologyOptions {{ BaseUrl = {BaseUrl}, Username = {Username}, Password = ***, VerifyTls = {VerifyTls} }}";
}
16 changes: 16 additions & 0 deletions tests/SynoSharp.Tests/SynologyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/// <summary>
Expand Down
Loading