diff --git a/src/RemoteAgent/Files/FileService.cs b/src/RemoteAgent/Files/FileService.cs index 0e2e998..8635489 100644 --- a/src/RemoteAgent/Files/FileService.cs +++ b/src/RemoteAgent/Files/FileService.cs @@ -75,7 +75,10 @@ private async Task HandleAsync(HttpListenerContext ctx) } catch (Exception ex) { - try { ctx.Response.StatusCode = 500; var b = Encoding.UTF8.GetBytes(ex.Message); ctx.Response.OutputStream.Write(b); ctx.Response.Close(); } catch { /* client gone */ } + // Log the detail server-side; return a generic, non-reflective body so no request-controlled + // data is echoed back into the response. + logger.LogWarning(ex, "File service request failed."); + try { ctx.Response.StatusCode = 500; ctx.Response.ContentType = "text/plain; charset=utf-8"; var b = Encoding.UTF8.GetBytes("Operation failed."); ctx.Response.OutputStream.Write(b); ctx.Response.Close(); } catch { /* client gone */ } } } @@ -139,7 +142,8 @@ private static async Task WriteJsonAsync(HttpListenerContext ctx, FsList list) private static void Ok(HttpListenerContext ctx) { ctx.Response.StatusCode = 200; ctx.Response.Close(); } - private void Audit(string op, string detail) => logger.LogInformation("file {Op}: {Detail}", op, detail); + // Strip CR/LF from the request-controlled detail so a crafted path cannot forge extra audit-log lines. + private void Audit(string op, string detail) => logger.LogInformation("file {Op}: {Detail}", op, detail.Replace('\r', ' ').Replace('\n', ' ')); /// Logged-in user's home (best-effort): the system drive's Users\<active console user>, else the drive root. private static string HomeDir() diff --git a/src/RemoteAgent/RemoteAgent.csproj b/src/RemoteAgent/RemoteAgent.csproj index 786ac1c..e57f9b6 100644 --- a/src/RemoteAgent/RemoteAgent.csproj +++ b/src/RemoteAgent/RemoteAgent.csproj @@ -7,7 +7,7 @@ enable RemoteAgent RemoteAgent - 1.7.0.0 + 1.7.1.0 ..\..\icon\app.ico diff --git a/src/RemoteAgent/Services/VncProvisioningService.cs b/src/RemoteAgent/Services/VncProvisioningService.cs index 714c298..0d1b81b 100644 --- a/src/RemoteAgent/Services/VncProvisioningService.cs +++ b/src/RemoteAgent/Services/VncProvisioningService.cs @@ -1,4 +1,6 @@ using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Text; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -37,7 +39,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) try { var secretFile = Path.Combine(_opt.EnrollmentDir, "vnc.secret"); - string? password = File.Exists(secretFile) ? File.ReadAllText(secretFile).Trim() : null; + string? password = ReadSecret(secretFile); var msi = Path.Combine(AppContext.BaseDirectory, "vnc", "tightvnc.msi"); @@ -48,7 +50,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await VncProvisioner.EnsureInstalledAsync(msi); VncProvisioner.ApplyHardening(password); - File.WriteAllText(secretFile, password); + WriteSecret(secretFile, password); logger.LogInformation(L.VncProvisioningService_VNCProvisionedPerDevicePassword); } catch (Exception ex) @@ -87,7 +89,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) try { var secretFile = Path.Combine(_opt.EnrollmentDir, "vnc.secret"); - var pw = File.Exists(secretFile) ? File.ReadAllText(secretFile).Trim() : null; + var pw = ReadSecret(secretFile); if (!string.IsNullOrEmpty(pw)) await VncProvisioner.EnsureHealthyAsync(pw, msiPath); } @@ -95,6 +97,25 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } } + // vnc.secret stores the local VNC password. Encrypt it at rest with DPAPI (machine scope; the agent + // runs as SYSTEM), transparently migrating any legacy plaintext file the first time it is read. + private static string? ReadSecret(string file) + { + if (!File.Exists(file)) return null; + var raw = File.ReadAllBytes(file); + if (raw.Length == 0) return null; + try { return Encoding.UTF8.GetString(ProtectedData.Unprotect(raw, null, DataProtectionScope.LocalMachine)).Trim(); } + catch + { + var plain = Encoding.UTF8.GetString(raw).Trim(); // legacy plaintext + try { WriteSecret(file, plain); } catch { /* best-effort migration */ } + return plain; + } + } + + private static void WriteSecret(string file, string password) => + File.WriteAllBytes(file, ProtectedData.Protect(Encoding.UTF8.GetBytes(password), null, DataProtectionScope.LocalMachine)); + private async Task ReportSecretAsync(string password, CancellationToken ct) { if (string.IsNullOrWhiteSpace(_opt.Telemetry.IngestUrl))