From 02820e62e197daa66199e3cfccbf12c2c5d363cc Mon Sep 17 00:00:00 2001 From: Xenne <144433308+Xenne93@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:52:17 +0200 Subject: [PATCH 1/4] Fix GitHub Code Scanning alerts (Security and quality) - Add explicit permissions blocks to build-check.yml and remove-old-packages.yml workflows (actions/missing-workflow-permissions) - Add missing admin check to PanelSettingsController.PurgeData, which previously let any authenticated user (including moderators) purge all logged data including audit logs - Add path-containment validation to MapStorageService (GetServerImageFilePath) using Path.GetFullPath + base-directory prefix check, closing an unauthenticated arbitrary-file-read via PublicImagesController's fast disk-read path using an unvalidated instanceHash route parameter (cs/path-injection) - Harden Discord webhook URL validation in ServerWebhookController.TestWebhook to check uri.Host/AbsolutePath explicitly instead of a raw substring .Contains() match - Clean up RconController.DeleteServer to return NotFound on a missing/ non-owned server instead of relying on a NullReferenceException falling through to a broad catch block - Add a shared LogSanitizer helper and use it to strip CR/LF from every user-controlled string value before it reaches a logger call across 18 files (cs/log-forging), converting any remaining string-interpolated log calls to structured logging with named placeholders along the way - Replace User.GetEmail() with a non-PII user identifier (resolved user object's Id, or the NameIdentifier claim) in every log call that previously logged the user's email address (cs/exposure-of-sensitive-information) - Mask the recipient address in EmailService's log calls instead of logging it in full - Dismiss 4 alerts confirmed as false positives via the Code Scanning API, each with a documented reason (cs/cleartext-storage-of-sensitive-information #7, cs/user-controlled-bypass #4, #5, #6) --- .github/workflows/build-check.yml | 3 ++ .github/workflows/remove-old-packages.yml | 4 ++ .../Controllers/AuthController.cs | 4 +- .../Controllers/DashboardController.cs | 22 +++++------ .../Controllers/ModsController.cs | 26 ++++++------- .../Controllers/PanelSettingsController.cs | 36 +++++++++-------- .../PermissionsManagerController.cs | 12 +++--- .../Controllers/PlayerInspectController.cs | 17 ++++---- .../Controllers/PlayerNotesController.cs | 11 +++--- .../Controllers/PresetCommandsController.cs | 22 ++++++----- .../Controllers/PublicImagesController.cs | 11 +++++- .../RconController.ServerManagement.cs | 23 +++++------ .../Controllers/SchedulerController.cs | 39 ++++++++++--------- .../Controllers/ServerWebhookController.cs | 14 ++++--- .../Controllers/StatsController.cs | 5 ++- .../Controllers/TriggersController.cs | 21 +++++----- .../Helpers/LogSanitizer.cs | 21 ++++++++++ .../Services/AuditLogService.cs | 3 +- .../Services/DiscordWebhookService.cs | 33 ++++++++-------- .../Services/EmailService.cs | 19 ++++++++- .../Services/MapStorageService.cs | 29 ++++++++++---- .../Services/PluginVersionCheckService.cs | 39 ++++++++++--------- .../RconBackgroundService.Connections.cs | 17 ++++---- 23 files changed, 256 insertions(+), 175 deletions(-) create mode 100644 RustRconServerManager.Backend/Helpers/LogSanitizer.cs diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index 216bea3..053fb22 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -6,6 +6,9 @@ on: - main workflow_dispatch: +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/remove-old-packages.yml b/.github/workflows/remove-old-packages.yml index feebf7f..ff97f4d 100644 --- a/.github/workflows/remove-old-packages.yml +++ b/.github/workflows/remove-old-packages.yml @@ -4,6 +4,10 @@ on: - cron: '0 0 * * 0' # elke zondag workflow_dispatch: +permissions: + contents: read + packages: write + jobs: cleanup: runs-on: ubuntu-latest diff --git a/RustRconServerManager.Backend/Controllers/AuthController.cs b/RustRconServerManager.Backend/Controllers/AuthController.cs index 1d3b1d9..d4b639d 100644 --- a/RustRconServerManager.Backend/Controllers/AuthController.cs +++ b/RustRconServerManager.Backend/Controllers/AuthController.cs @@ -472,7 +472,7 @@ private async Task> GetAccessibleServers(ApplicationUser user) [HttpPost("forgot-password")] public async Task ForgotPassword(Authorization_ForgotPasswordDTO model) { - _logger.LogDebug("[FORGOT-PASSWORD] Request received for email: {Email}", model.Email); + _logger.LogDebug("[FORGOT-PASSWORD] Request received"); // Always return 200 to prevent email enumeration var genericMessage = "If an account with that email exists, a recovery code has been sent."; @@ -490,7 +490,7 @@ public async Task ForgotPassword(Authorization_ForgotPasswordDTO return Ok(new { message = genericMessage }); } - _logger.LogDebug("[FORGOT-PASSWORD] User found: {Email}", user.Email); + _logger.LogDebug("[FORGOT-PASSWORD] User found: {UserId}", user.Id); // Generate random 6-digit code (overwrites any existing code) var code = RandomNumberGenerator.GetInt32(100000, 999999).ToString(); diff --git a/RustRconServerManager.Backend/Controllers/DashboardController.cs b/RustRconServerManager.Backend/Controllers/DashboardController.cs index 9fad694..4a2cb01 100644 --- a/RustRconServerManager.Backend/Controllers/DashboardController.cs +++ b/RustRconServerManager.Backend/Controllers/DashboardController.cs @@ -352,12 +352,12 @@ public async Task BanPlayer([FromBody] BanPlayerRequest request) { string banCommand = $"banid {request.SteamId} \"{request.PlayerName}\" \"[GLOBAL BAN] {request.Reason}\" {request.DurationHours}"; _rconService.SendRconCommand(banCommand, server.Id); - _logger.LogInformation("Sent global ban command to server {ServerName} (ID: {ServerId}) for player {PlayerName}", server.Name, server.Id, request.PlayerName); + _logger.LogInformation("Sent global ban command to server {ServerName} (ID: {ServerId}) for player {PlayerName}", LogSanitizer.Sanitize(server.Name), server.Id, LogSanitizer.Sanitize(request.PlayerName)); } catch (Exception ex) { // Log but don't fail if server is offline - ban is still tracked in database - _logger.LogWarning(ex, "Could not send global ban command to server {ServerName}", server.Name); + _logger.LogWarning(ex, "Could not send global ban command to server {ServerName}", LogSanitizer.Sanitize(server.Name)); } } } @@ -870,7 +870,7 @@ public async Task ToggleGlobalBan(int banId, [FromBody] ToggleGlo // If toggling OFF a global ban, unban from all servers if (ban.IsGlobalBan && !request.IsGlobalBan) { - _logger.LogInformation("[GLOBAL UNBAN] Toggling off global ban for {SteamId}. Unbanning from all servers...", ban.SteamId); + _logger.LogInformation("[GLOBAL UNBAN] Toggling off global ban for {SteamId}. Unbanning from all servers...", LogSanitizer.Sanitize(ban.SteamId)); // Get current user email for audit trail var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "System"; @@ -902,7 +902,7 @@ public async Task ToggleGlobalBan(int banId, [FromBody] ToggleGlo { string unbanCommand = $"unban {playerBan.SteamId}"; _rconService.SendRconCommand(unbanCommand, playerBan.ServerId); - _logger.LogInformation("[GLOBAL UNBAN] Sent unban command for {SteamId} on server {ServerId}", ban.SteamId, playerBan.ServerId); + _logger.LogInformation("[GLOBAL UNBAN] Sent unban command for {SteamId} on server {ServerId}", LogSanitizer.Sanitize(ban.SteamId), playerBan.ServerId); } catch (Exception ex) { @@ -914,7 +914,7 @@ public async Task ToggleGlobalBan(int banId, [FromBody] ToggleGlo } await dbContext.SaveChangesAsync(); - _logger.LogInformation("[GLOBAL UNBAN] Removed all bans for {SteamId} from database", ban.SteamId); + _logger.LogInformation("[GLOBAL UNBAN] Removed all bans for {SteamId} from database", LogSanitizer.Sanitize(ban.SteamId)); return Ok(new { message = $"Player {ban.SteamId} has been unbanned from all servers" }); } @@ -966,7 +966,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques // If this is a global ban, unban from all servers if (ban.IsGlobalBan && ban.ServerId == -1) { - _logger.LogInformation("[GLOBAL UNBAN] Removing global ban for {SteamId}", ban.SteamId); + _logger.LogInformation("[GLOBAL UNBAN] Removing global ban for {SteamId}", LogSanitizer.Sanitize(ban.SteamId)); // Find all server-specific bans created by this global ban var relatedServerBans = await dbContext.PlayerBans @@ -994,7 +994,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques { string unbanCommand = $"unban {serverBan.SteamId}"; _rconService.SendRconCommand(unbanCommand, serverBan.ServerId); - _logger.LogInformation("[GLOBAL UNBAN] Sent unban command for {SteamId} on server {ServerId}", ban.SteamId, serverBan.ServerId); + _logger.LogInformation("[GLOBAL UNBAN] Sent unban command for {SteamId} on server {ServerId}", LogSanitizer.Sanitize(ban.SteamId), serverBan.ServerId); } catch (Exception ex) { @@ -1057,7 +1057,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques { string unbanCommand = $"unban {ban.SteamId}"; _rconService.SendRconCommand(unbanCommand, ban.ServerId); - _logger.LogInformation("[UNBAN] Sent unban command for SteamID {SteamId} on server {ServerId}", ban.SteamId, ban.ServerId); + _logger.LogInformation("[UNBAN] Sent unban command for SteamID {SteamId} on server {ServerId}", LogSanitizer.Sanitize(ban.SteamId), ban.ServerId); } catch (Exception ex) { @@ -1068,7 +1068,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques // Remove the ban record from database dbContext.PlayerBans.Remove(ban); await dbContext.SaveChangesAsync(); - _logger.LogInformation("[UNBAN] Removed ban record for SteamID {SteamId} from database", ban.SteamId); + _logger.LogInformation("[UNBAN] Removed ban record for SteamID {SteamId} from database", LogSanitizer.Sanitize(ban.SteamId)); return Ok(new { message = "Player unbanned successfully on server and database updated" }); } @@ -1342,12 +1342,12 @@ public async Task GiveItem([FromBody] GiveItemRequest request) // Build the RCON command: inventory.giveto var command = $"inventory.giveto {request.SteamId} {request.ShortName} {request.Quantity}"; - _logger.LogInformation("[GIVE ITEM] Executing command: {Command} for player {PlayerName}", command, request.PlayerName); + _logger.LogInformation("[GIVE ITEM] Executing command: {Command} for player {PlayerName}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(request.PlayerName)); // Execute the RCON command var response = await _rconService.ExecuteCommandWithResponse(command, request.ServerId); - _logger.LogDebug("[GIVE ITEM] Command response: {Response}", response); + _logger.LogDebug("[GIVE ITEM] Command response: {Response}", LogSanitizer.Sanitize(response)); return Ok(new { success = true, diff --git a/RustRconServerManager.Backend/Controllers/ModsController.cs b/RustRconServerManager.Backend/Controllers/ModsController.cs index a5d600f..e78b120 100644 --- a/RustRconServerManager.Backend/Controllers/ModsController.cs +++ b/RustRconServerManager.Backend/Controllers/ModsController.cs @@ -77,7 +77,7 @@ public async Task GetPlugins() // Execute RCON command to get plugins string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", command, rconResponse ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); if (string.IsNullOrWhiteSpace(rconResponse)) { @@ -182,12 +182,12 @@ public async Task LoadPlugin([FromBody] Mods_ReloadPluginDTO requ ? $"c.load {request.PluginName}" : $"o.load {request.PluginName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", command, serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", command, rconResponse ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); return Ok(new { @@ -249,12 +249,12 @@ public async Task ReloadPlugin([FromBody] Mods_ReloadPluginDTO re ? $"c.reload {request.PluginName}" : $"o.reload {request.PluginName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", command, serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", command, rconResponse ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); return Ok(new { @@ -316,12 +316,12 @@ public async Task UnloadPlugin([FromBody] Mods_UnloadPluginDTO re ? $"c.unload {request.PluginName}" : $"o.unload {request.PluginName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", command, serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", command, rconResponse ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); return Ok(new { @@ -377,12 +377,12 @@ public async Task ReloadAll() ? "c.reload *" : "o.reload *"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", command, serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", command, rconResponse ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); return Ok(new { @@ -444,7 +444,7 @@ public async Task SetPluginSource([FromBody] Mods_SetPluginSource _dbContext.ServerPluginSources.Remove(existingEntry); await _dbContext.SaveChangesAsync(); - _logger.LogInformation("Removed plugin source for {PluginName} on server {ServerId}", request.PluginName, serverId); + _logger.LogInformation("Removed plugin source for {PluginName} on server {ServerId}", LogSanitizer.Sanitize(request.PluginName), serverId); return Ok(new { @@ -468,7 +468,7 @@ public async Task SetPluginSource([FromBody] Mods_SetPluginSource await _dbContext.SaveChangesAsync(); _logger.LogInformation("Updated plugin source for {PluginName} on server {ServerId} to {Source}", - request.PluginName, serverId, request.Source.Value); + LogSanitizer.Sanitize(request.PluginName), serverId, request.Source.Value); return Ok(new { @@ -491,7 +491,7 @@ public async Task SetPluginSource([FromBody] Mods_SetPluginSource await _dbContext.SaveChangesAsync(); _logger.LogInformation("Created plugin source for {PluginName} on server {ServerId} as {Source}", - request.PluginName, serverId, request.Source.Value); + LogSanitizer.Sanitize(request.PluginName), serverId, request.Source.Value); return Ok(new { @@ -636,7 +636,7 @@ public async Task CheckPluginVersion([FromQuery] string pluginNam } catch (Exception ex) { - _logger.LogError(ex, $"Error checking version for plugin {pluginName}"); + _logger.LogError(ex, "Error checking version for plugin {PluginName}", LogSanitizer.Sanitize(pluginName)); return StatusCode(500, ApiErrorHelper.FormatError("Error checking plugin version", ex)); } } diff --git a/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs b/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs index ceb4b0e..53af50e 100644 --- a/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs +++ b/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; using RustRconServerManager.Backend.Services; using RustRconServerManager.Shared.Scheduler; @@ -69,7 +70,7 @@ public async Task> GetMySettings() _dbContext.PanelSettings.Add(panelSettings); await _dbContext.SaveChangesAsync(); - _logger.LogInformation($"[PanelSettingsController] Created default panel settings for SystemProfile {applicationUser.SystemProfileId}"); + _logger.LogInformation("[PanelSettingsController] Created default panel settings for SystemProfile {SystemProfileId}", applicationUser.SystemProfileId); } return Ok(MapToDto(panelSettings)); @@ -164,6 +165,9 @@ public async Task PurgeData([FromBody] PurgeDataRequestDto reques var applicationUser = await _dbContext.Users.FindAsync(userId); if (applicationUser == null) return NotFound("User not found"); + if (!applicationUser.isAdmin) + return Forbid(); + var serverIds = await _dbContext.RconServers .Where(s => s.SystemProfileId == applicationUser.SystemProfileId) .Select(s => s.Id) @@ -215,7 +219,7 @@ public async Task PurgeData([FromBody] PurgeDataRequestDto reques } _logger.LogInformation("[PanelSettingsController] User {UserId} purged {Count} records from {Category} (older than {Days} days)", - userId, deleted, request.Category, request.OlderThanDays); + userId, deleted, LogSanitizer.Sanitize(request.Category), request.OlderThanDays); return Ok(new { deleted, category = request.Category }); } @@ -256,8 +260,8 @@ public async Task> SetLogLevel([FromBody] SetLogL LogLevelState.Minimum = parsedLevel; - _logger.LogInformation("[PanelSettingsController] Minimum log level changed to {Level} by {User}", - parsedLevel, currentUser.Email); + _logger.LogInformation("[PanelSettingsController] Minimum log level changed to {Level} by {UserId}", + parsedLevel, currentUser.Id); return Ok(MapToDto(panelSettings)); } @@ -295,8 +299,8 @@ public async Task> SetAutoUpdate([FromBody] SetAu _autoUpdateFlagFileService.Write(dto.AutoUpdateEnabled); - _logger.LogInformation("[PanelSettingsController] Auto-update {State} by {User}", - dto.AutoUpdateEnabled ? "enabled" : "disabled", currentUser.Email); + _logger.LogInformation("[PanelSettingsController] Auto-update {State} by {UserId}", + dto.AutoUpdateEnabled ? "enabled" : "disabled", currentUser.Id); return Ok(MapToDto(panelSettings)); } @@ -330,8 +334,8 @@ public async Task> SetDeveloperMode([FromBody] Se panelSettings.UpdatedAt = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); - _logger.LogInformation("[PanelSettingsController] Developer mode {State} by {User}", - dto.DeveloperModeEnabled ? "enabled" : "disabled", currentUser.Email); + _logger.LogInformation("[PanelSettingsController] Developer mode {State} by {UserId}", + dto.DeveloperModeEnabled ? "enabled" : "disabled", currentUser.Id); return Ok(MapToDto(panelSettings)); } @@ -365,8 +369,8 @@ public async Task> SetAnalytics([FromBody] SetAna panelSettings.UpdatedAt = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); - _logger.LogInformation("[PanelSettingsController] Anonymous analytics {State} by {User}", - dto.AnalyticsEnabled ? "enabled" : "disabled", currentUser.Email); + _logger.LogInformation("[PanelSettingsController] Anonymous analytics {State} by {UserId}", + dto.AnalyticsEnabled ? "enabled" : "disabled", currentUser.Id); return Ok(MapToDto(panelSettings)); } @@ -424,8 +428,8 @@ public async Task> SetSteamApiKey([FromBody] SetS panelSettings.UpdatedAt = DateTime.UtcNow; await _dbContext.SaveChangesAsync(); - _logger.LogInformation("[PanelSettingsController] Steam API key {Action} by {User}", - dto.RemoveKey ? "removed" : "updated", currentUser.Email); + _logger.LogInformation("[PanelSettingsController] Steam API key {Action} by {UserId}", + dto.RemoveKey ? "removed" : "updated", currentUser.Id); return Ok(MapToDto(panelSettings)); } @@ -508,8 +512,8 @@ public async Task> UpsertVacBanOverride await _dbContext.SaveChangesAsync(); - _logger.LogInformation("[PanelSettingsController] VAC-ban override upserted for SteamID {SteamId} by {User}", - steamId, currentUser.Email); + _logger.LogInformation("[PanelSettingsController] VAC-ban override upserted for SteamID {SteamId} by {UserId}", + steamId, currentUser.Id); return Ok(new DeveloperVacBanOverrideDto { @@ -547,8 +551,8 @@ public async Task DeleteVacBanOverride(string steamId) if (deleted == 0) return NotFound("Override not found"); - _logger.LogInformation("[PanelSettingsController] VAC-ban override removed for SteamID {SteamId} by {User}", - steamId, currentUser.Email); + _logger.LogInformation("[PanelSettingsController] VAC-ban override removed for SteamID {SteamId} by {UserId}", + steamId, currentUser.Id); return Ok(); } diff --git a/RustRconServerManager.Backend/Controllers/PermissionsManagerController.cs b/RustRconServerManager.Backend/Controllers/PermissionsManagerController.cs index 29ca130..ed664e3 100644 --- a/RustRconServerManager.Backend/Controllers/PermissionsManagerController.cs +++ b/RustRconServerManager.Backend/Controllers/PermissionsManagerController.cs @@ -153,7 +153,7 @@ public async Task GetGroupPlayers([FromQuery] string groupName) // Execute RCON command to get group details string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", command, rconResponse ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); if (string.IsNullOrWhiteSpace(rconResponse)) { @@ -163,7 +163,7 @@ public async Task GetGroupPlayers([FromQuery] string groupName) // Parse the RCON response to extract players var players = ParsePlayersFromGroupResponse(rconResponse, groupName); - _logger.LogInformation("Parsed {Count} players for group {GroupName}", players.Count, groupName); + _logger.LogInformation("Parsed {Count} players for group {GroupName}", players.Count, LogSanitizer.Sanitize(groupName)); return Ok(players); } @@ -277,12 +277,12 @@ public async Task AddPlayerToGroup([FromBody] PermissionsManager_ ? $"c.usergroup add {request.SteamId} {request.GroupName}" : $"o.usergroup add {request.SteamId} {request.GroupName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", command, serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", command, rconResponse ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); return Ok(new { message = $"Player {request.SteamId} added to group {request.GroupName}", @@ -342,12 +342,12 @@ public async Task RemovePlayerFromGroup([FromBody] PermissionsMan ? $"c.usergroup remove {request.SteamId} {request.GroupName}" : $"o.usergroup remove {request.SteamId} {request.GroupName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", command, serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", command, rconResponse ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); return Ok(new { message = $"Player {request.SteamId} removed from group {request.GroupName}", diff --git a/RustRconServerManager.Backend/Controllers/PlayerInspectController.cs b/RustRconServerManager.Backend/Controllers/PlayerInspectController.cs index 87e5e21..0db99ab 100644 --- a/RustRconServerManager.Backend/Controllers/PlayerInspectController.cs +++ b/RustRconServerManager.Backend/Controllers/PlayerInspectController.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Interfaces; using RustRconServerManager.Shared.PlayerInspect; @@ -210,7 +211,7 @@ public async Task GetPlayerData(string steamId) } catch (Exception ex) { - _logger.LogError(ex, $"Error fetching player data for Steam ID {steamId}"); + _logger.LogError(ex, "Error fetching player data for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); return StatusCode(500, new { message = "Internal server error" }); } } @@ -257,7 +258,7 @@ public async Task GetPlayerChatMessages(string steamId, [FromQuer } catch (Exception ex) { - _logger.LogError(ex, $"Error fetching chat messages for Steam ID {steamId}"); + _logger.LogError(ex, "Error fetching chat messages for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); return StatusCode(500, new { message = "Internal server error" }); } } @@ -304,7 +305,7 @@ public async Task SearchPlayerChatMessages(string steamId, [FromQ } catch (Exception ex) { - _logger.LogError(ex, $"Error searching chat messages for Steam ID {steamId}"); + _logger.LogError(ex, "Error searching chat messages for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); return StatusCode(500, new { message = "Internal server error" }); } } @@ -354,7 +355,7 @@ public async Task GetPlayerKills(string steamId, [FromQuery] int } catch (Exception ex) { - _logger.LogError(ex, $"Error fetching kills for Steam ID {steamId}"); + _logger.LogError(ex, "Error fetching kills for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); return StatusCode(500, new { message = "Internal server error" }); } } @@ -404,7 +405,7 @@ public async Task GetPlayerDeaths(string steamId, [FromQuery] int } catch (Exception ex) { - _logger.LogError(ex, $"Error fetching deaths for Steam ID {steamId}"); + _logger.LogError(ex, "Error fetching deaths for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); return StatusCode(500, new { message = "Internal server error" }); } } @@ -496,7 +497,7 @@ public async Task ClearPlayerData(string steamId, [FromBody] Clea await dbContext.SaveChangesAsync(); _logger.LogInformation("[PlayerInspect] Cleared data for {SteamId} on server {ServerId}: Chat={Chat}, Kills={Kills}, Deaths={Deaths}, Notes={Notes}, Reports={Reports}, BanHistory={BanHistory}, PlayerDeleted={Deleted}", - steamId, request.ServerId, deletedChat, deletedKills, deletedDeaths, deletedNotes, deletedReports, deletedBanHistory, request.DeletePlayer); + LogSanitizer.Sanitize(steamId), request.ServerId, deletedChat, deletedKills, deletedDeaths, deletedNotes, deletedReports, deletedBanHistory, request.DeletePlayer); return Ok(new { @@ -512,7 +513,7 @@ public async Task ClearPlayerData(string steamId, [FromBody] Clea } catch (Exception ex) { - _logger.LogError(ex, "Error clearing player data for {SteamId}", steamId); + _logger.LogError(ex, "Error clearing player data for {SteamId}", LogSanitizer.Sanitize(steamId)); return StatusCode(500, new { message = "Internal server error" }); } } @@ -562,7 +563,7 @@ public async Task GetPlayerInventory(string steamId) } catch (Exception ex) { - _logger.LogError(ex, "Error getting player inventory for {SteamId}", steamId); + _logger.LogError(ex, "Error getting player inventory for {SteamId}", LogSanitizer.Sanitize(steamId)); return StatusCode(500, "An error occurred while fetching the inventory."); } } diff --git a/RustRconServerManager.Backend/Controllers/PlayerNotesController.cs b/RustRconServerManager.Backend/Controllers/PlayerNotesController.cs index 9702d57..d5d3b5f 100644 --- a/RustRconServerManager.Backend/Controllers/PlayerNotesController.cs +++ b/RustRconServerManager.Backend/Controllers/PlayerNotesController.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; using RustRconServerManager.Shared.PlayerNotes; using System.Security.Claims; @@ -61,7 +62,7 @@ public async Task> GetPlayerNotes(string st } catch (Exception ex) { - _logger.LogError(ex, "[PlayerNotesController] Error getting notes for player {SteamId}", steamId); + _logger.LogError(ex, "[PlayerNotesController] Error getting notes for player {SteamId}", LogSanitizer.Sanitize(steamId)); return StatusCode(500, new { error = "Error retrieving player notes" }); } } @@ -100,7 +101,7 @@ public async Task> CreatePlayerNote([FromBody] Creat await _dbContext.SaveChangesAsync(); _logger.LogInformation("[PlayerNotesController] User {UserId} created note {NoteId} for player {SteamId} on server {ServerId}", - userId, playerNote.Id, request.SteamId, request.ServerId); + userId, playerNote.Id, LogSanitizer.Sanitize(request.SteamId), request.ServerId); var dto = new PlayerNoteDTO { @@ -117,7 +118,7 @@ public async Task> CreatePlayerNote([FromBody] Creat } catch (Exception ex) { - _logger.LogError(ex, "[PlayerNotesController] Error creating note for player {SteamId}", request.SteamId); + _logger.LogError(ex, "[PlayerNotesController] Error creating note for player {SteamId}", LogSanitizer.Sanitize(request.SteamId)); return StatusCode(500, new { error = "Error creating player note" }); } } @@ -155,7 +156,7 @@ public async Task> UpdatePlayerNote(int noteId, [Fro var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; _logger.LogInformation("[PlayerNotesController] User {UserId} updated note {NoteId} for player {SteamId}", - userId, noteId, playerNote.SteamId); + userId, noteId, LogSanitizer.Sanitize(playerNote.SteamId)); var dto = new PlayerNoteDTO { @@ -202,7 +203,7 @@ public async Task DeletePlayerNote(int noteId) var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; _logger.LogInformation("[PlayerNotesController] User {UserId} deleted note {NoteId} for player {SteamId}", - userId, noteId, playerNote.SteamId); + userId, noteId, LogSanitizer.Sanitize(playerNote.SteamId)); return NoContent(); } diff --git a/RustRconServerManager.Backend/Controllers/PresetCommandsController.cs b/RustRconServerManager.Backend/Controllers/PresetCommandsController.cs index a7563cc..10d778a 100644 --- a/RustRconServerManager.Backend/Controllers/PresetCommandsController.cs +++ b/RustRconServerManager.Backend/Controllers/PresetCommandsController.cs @@ -3,8 +3,10 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; using RustRconServerManager.Shared.PresetCommand; +using System.Security.Claims; namespace RustRconServerManager.Backend.Controllers { @@ -34,7 +36,7 @@ public async Task>> GetPresetCommands(int se { if (!await User.HasServerAccess(_dbContext, serverId)) { - _logger.LogWarning($"[PresetCommandsController] User {User.GetEmail()} attempted to access server {serverId} without authorization"); + _logger.LogWarning("[PresetCommandsController] User {UserId} attempted to access server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, serverId); return Forbid(); } @@ -49,7 +51,7 @@ public async Task>> GetPresetCommands(int se } catch (Exception ex) { - _logger.LogError(ex, $"[PresetCommandsController] Error getting preset commands for server {serverId}"); + _logger.LogError(ex, "[PresetCommandsController] Error getting preset commands for server {ServerId}", serverId); return StatusCode(500, new { error = "Failed to retrieve preset commands" }); } } @@ -73,7 +75,7 @@ public async Task> CreatePresetCommand([FromBody] { if (!await User.HasServerAccess(_dbContext, dto.RconServerId.Value)) { - _logger.LogWarning($"[PresetCommandsController] User {User.GetEmail()} attempted to create preset for server {dto.RconServerId} without authorization"); + _logger.LogWarning("[PresetCommandsController] User {UserId} attempted to create preset for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, dto.RconServerId); return Forbid(); } } @@ -92,7 +94,7 @@ public async Task> CreatePresetCommand([FromBody] _dbContext.PresetCommands.Add(command); await _dbContext.SaveChangesAsync(); - _logger.LogInformation($"[PresetCommandsController] User {User.GetEmail()} created preset command {command.Id} '{command.Name}'"); + _logger.LogInformation("[PresetCommandsController] User {UserId} created preset command {PresetId} '{PresetName}'", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, command.Id, LogSanitizer.Sanitize(command.Name)); return CreatedAtAction(nameof(GetPresetCommands), new { serverId = command.RconServerId ?? 0 }, MapToDto(command)); } @@ -118,7 +120,7 @@ public async Task> UpdatePresetCommand(int id, [F // Verify access to the server the preset belongs to if (existing.RconServerId.HasValue && !await User.HasServerAccess(_dbContext, existing.RconServerId.Value)) { - _logger.LogWarning($"[PresetCommandsController] User {User.GetEmail()} attempted to update preset {id} without authorization"); + _logger.LogWarning("[PresetCommandsController] User {UserId} attempted to update preset {PresetId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id); return Forbid(); } @@ -138,13 +140,13 @@ public async Task> UpdatePresetCommand(int id, [F await _dbContext.SaveChangesAsync(); - _logger.LogInformation($"[PresetCommandsController] User {User.GetEmail()} updated preset command {id}"); + _logger.LogInformation("[PresetCommandsController] User {UserId} updated preset command {PresetId}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id); return Ok(MapToDto(existing)); } catch (Exception ex) { - _logger.LogError(ex, $"[PresetCommandsController] Error updating preset command {id}"); + _logger.LogError(ex, "[PresetCommandsController] Error updating preset command {PresetId}", id); return StatusCode(500, new { error = "Failed to update preset command" }); } } @@ -164,20 +166,20 @@ public async Task DeletePresetCommand(int id) // Verify access to the server the preset belongs to if (command.RconServerId.HasValue && !await User.HasServerAccess(_dbContext, command.RconServerId.Value)) { - _logger.LogWarning($"[PresetCommandsController] User {User.GetEmail()} attempted to delete preset {id} without authorization"); + _logger.LogWarning("[PresetCommandsController] User {UserId} attempted to delete preset {PresetId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id); return Forbid(); } _dbContext.PresetCommands.Remove(command); await _dbContext.SaveChangesAsync(); - _logger.LogInformation($"[PresetCommandsController] User {User.GetEmail()} deleted preset command {id}"); + _logger.LogInformation("[PresetCommandsController] User {UserId} deleted preset command {PresetId}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id); return NoContent(); } catch (Exception ex) { - _logger.LogError(ex, $"[PresetCommandsController] Error deleting preset command {id}"); + _logger.LogError(ex, "[PresetCommandsController] Error deleting preset command {PresetId}", id); return StatusCode(500, new { error = "Failed to delete preset command" }); } } diff --git a/RustRconServerManager.Backend/Controllers/PublicImagesController.cs b/RustRconServerManager.Backend/Controllers/PublicImagesController.cs index 92f113f..68ec459 100644 --- a/RustRconServerManager.Backend/Controllers/PublicImagesController.cs +++ b/RustRconServerManager.Backend/Controllers/PublicImagesController.cs @@ -45,7 +45,16 @@ public async Task GetServerImage(string instanceHash, int serverI } // Try to serve from disk first (fast) - var filePath = _mapStorageService.GetServerImageFilePath(instanceHash, serverId, filename); + string filePath; + try + { + filePath = _mapStorageService.GetServerImageFilePath(instanceHash, serverId, filename); + } + catch (ArgumentException) + { + return NotFound("Invalid image path."); + } + if (System.IO.File.Exists(filePath)) { var imageBytes = await System.IO.File.ReadAllBytesAsync(filePath); diff --git a/RustRconServerManager.Backend/Controllers/RconController.ServerManagement.cs b/RustRconServerManager.Backend/Controllers/RconController.ServerManagement.cs index 5f83f80..318941b 100644 --- a/RustRconServerManager.Backend/Controllers/RconController.ServerManagement.cs +++ b/RustRconServerManager.Backend/Controllers/RconController.ServerManagement.cs @@ -46,22 +46,17 @@ public async Task DeleteServer(int serverId) { RconServer server = await dbContext.RconServers.SingleOrDefaultAsync(s => s.Id == serverId && s.SystemProfileId == user.SystemProfileId); - - if (server.SystemProfileId != user.SystemProfileId) + + if (server == null) { - return Unauthorized("Server does not match users systemprofile."); + return NotFound("Server not found or does not belong to your systemprofile."); } - else - { - dbContext.RconServers.Remove(server); - await dbContext.SaveChangesAsync(); - await _rconBackgroundService.DisconnectServerAsync(server.Id); - await _rconBackgroundService.HandleDeleteServer(server); - return Ok("Server successfully deleted"); - } - - - + + dbContext.RconServers.Remove(server); + await dbContext.SaveChangesAsync(); + await _rconBackgroundService.DisconnectServerAsync(server.Id); + await _rconBackgroundService.HandleDeleteServer(server); + return Ok("Server successfully deleted"); } diff --git a/RustRconServerManager.Backend/Controllers/SchedulerController.cs b/RustRconServerManager.Backend/Controllers/SchedulerController.cs index 39be0b1..8ead8d9 100644 --- a/RustRconServerManager.Backend/Controllers/SchedulerController.cs +++ b/RustRconServerManager.Backend/Controllers/SchedulerController.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Services; using RustRconServerManager.Backend.Models; using RustRconServerManager.Shared.Scheduler; @@ -44,7 +45,7 @@ public async Task>> GetScheduledCommands( { if (!await User.HasServerAccess(_dbContext, serverId)) { - _logger.LogWarning($"[SchedulerController] User {User.GetEmail()} attempted to access server {serverId} without authorization"); + _logger.LogWarning("[SchedulerController] User {UserId} attempted to access server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, serverId); return Forbid(); } @@ -55,7 +56,7 @@ public async Task>> GetScheduledCommands( } catch (Exception ex) { - _logger.LogError(ex, $"[SchedulerController] Error getting scheduled commands for server {serverId}"); + _logger.LogError(ex, "[SchedulerController] Error getting scheduled commands for server {ServerId}", serverId); return StatusCode(500, new { error = "Failed to retrieve scheduled commands" }); } } @@ -71,7 +72,7 @@ public async Task> CreateScheduledCommand([Fro { if (!await User.HasServerAccess(_dbContext, dto.RconServerId)) { - _logger.LogWarning($"[SchedulerController] User {User.GetEmail()} attempted to create command for server {dto.RconServerId} without authorization"); + _logger.LogWarning("[SchedulerController] User {UserId} attempted to create command for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, dto.RconServerId); return Forbid(); } @@ -105,7 +106,7 @@ public async Task> CreateScheduledCommand([Fro var utcOffset = dto.UtcOffsetMinutes ?? 0; var created = await _scheduledCommandService.CreateScheduledCommandAsync(command, utcOffset); - _logger.LogInformation($"[SchedulerController] User {User.GetEmail()} created scheduled command {created.Id} for server {created.RconServerId} (utcOffset={utcOffset})"); + _logger.LogInformation("[SchedulerController] User {UserId} created scheduled command {CommandId} for server {ServerId} (utcOffset={UtcOffset})", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, created.Id, created.RconServerId, utcOffset); return Ok(MapToDto(created)); } @@ -130,7 +131,7 @@ public async Task> UpdateScheduledCommand(int if (!await User.HasServerAccess(_dbContext, existing.RconServerId)) { - _logger.LogWarning($"[SchedulerController] User {User.GetEmail()} attempted to update command {id} for server {existing.RconServerId} without authorization"); + _logger.LogWarning("[SchedulerController] User {UserId} attempted to update command {CommandId} for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, existing.RconServerId); return Forbid(); } @@ -160,13 +161,13 @@ public async Task> UpdateScheduledCommand(int var utcOffset = dto.UtcOffsetMinutes ?? 0; var updated = await _scheduledCommandService.UpdateScheduledCommandAsync(existing, utcOffset); - _logger.LogInformation($"[SchedulerController] User {User.GetEmail()} updated scheduled command {id} (utcOffset={utcOffset})"); + _logger.LogInformation("[SchedulerController] User {UserId} updated scheduled command {CommandId} (utcOffset={UtcOffset})", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, utcOffset); return Ok(MapToDto(updated)); } catch (Exception ex) { - _logger.LogError(ex, $"[SchedulerController] Error updating scheduled command {id}"); + _logger.LogError(ex, "[SchedulerController] Error updating scheduled command {CommandId}", id); return StatusCode(500, new { error = "Failed to update scheduled command" }); } } @@ -185,19 +186,19 @@ public async Task DeleteScheduledCommand(int id) if (!await User.HasServerAccess(_dbContext, command.RconServerId)) { - _logger.LogWarning($"[SchedulerController] User {User.GetEmail()} attempted to delete command {id} for server {command.RconServerId} without authorization"); + _logger.LogWarning("[SchedulerController] User {UserId} attempted to delete command {CommandId} for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, command.RconServerId); return Forbid(); } await _scheduledCommandService.DeleteScheduledCommandAsync(id); - _logger.LogInformation($"[SchedulerController] User {User.GetEmail()} deleted scheduled command {id}"); + _logger.LogInformation("[SchedulerController] User {UserId} deleted scheduled command {CommandId}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id); return NoContent(); } catch (Exception ex) { - _logger.LogError(ex, $"[SchedulerController] Error deleting scheduled command {id}"); + _logger.LogError(ex, "[SchedulerController] Error deleting scheduled command {CommandId}", id); return StatusCode(500, new { error = "Failed to delete scheduled command" }); } } @@ -216,35 +217,35 @@ public async Task ExecuteNow(int id) if (!await User.HasServerAccess(_dbContext, command.RconServerId)) { - _logger.LogWarning($"[SchedulerController] User {User.GetEmail()} attempted to execute command {id} for server {command.RconServerId} without authorization"); + _logger.LogWarning("[SchedulerController] User {UserId} attempted to execute command {CommandId} for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, command.RconServerId); return Forbid(); } if (!_rconConnectionManager.TryGetClient(command.RconServerId, out var client) || !client.IsConnected) { - _logger.LogWarning($"[SchedulerController] Server {command.RconServerId} is not connected"); + _logger.LogWarning("[SchedulerController] Server {ServerId} is not connected", command.RconServerId); await _scheduledCommandService.MarkAsExecutedAsync(id, false, "Server not connected"); return BadRequest(new { error = "Server is not connected" }); } try { - _logger.LogInformation($"[SchedulerController] User {User.GetEmail()} executing command {id} ({command.Name}) manually: {command.Command}"); + _logger.LogInformation("[SchedulerController] User {UserId} executing command {CommandId} ({CommandName}) manually: {Command}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, LogSanitizer.Sanitize(command.Name), LogSanitizer.Sanitize(command.Command)); await client.SendCommandAsync(command.Command); - _logger.LogInformation($"[SchedulerController] Command {id} executed successfully"); + _logger.LogInformation("[SchedulerController] Command {CommandId} executed successfully", id); return Ok(new { message = "Command executed successfully" }); } catch (Exception sendEx) { - _logger.LogError(sendEx, $"[SchedulerController] Failed to send command {id} to RCON"); + _logger.LogError(sendEx, "[SchedulerController] Failed to send command {CommandId} to RCON", id); return BadRequest(new { error = $"Failed to execute command: {sendEx.Message}" }); } } catch (Exception ex) { - _logger.LogError(ex, $"[SchedulerController] Error executing command {id}"); + _logger.LogError(ex, "[SchedulerController] Error executing command {CommandId}", id); return StatusCode(500, new { error = "Failed to execute command" }); } } @@ -263,20 +264,20 @@ public async Task> ToggleActive(int id) if (!await User.HasServerAccess(_dbContext, command.RconServerId)) { - _logger.LogWarning($"[SchedulerController] User {User.GetEmail()} attempted to toggle command {id} for server {command.RconServerId} without authorization"); + _logger.LogWarning("[SchedulerController] User {UserId} attempted to toggle command {CommandId} for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, command.RconServerId); return Forbid(); } command.IsActive = !command.IsActive; var updated = await _scheduledCommandService.UpdateScheduledCommandAsync(command); - _logger.LogInformation($"[SchedulerController] User {User.GetEmail()} toggled active status of command {id} to {updated.IsActive}"); + _logger.LogInformation("[SchedulerController] User {UserId} toggled active status of command {CommandId} to {IsActive}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, updated.IsActive); return Ok(MapToDto(updated)); } catch (Exception ex) { - _logger.LogError(ex, $"[SchedulerController] Error toggling active status of command {id}"); + _logger.LogError(ex, "[SchedulerController] Error toggling active status of command {CommandId}", id); return StatusCode(500, new { error = "Failed to toggle active status" }); } } diff --git a/RustRconServerManager.Backend/Controllers/ServerWebhookController.cs b/RustRconServerManager.Backend/Controllers/ServerWebhookController.cs index f7c2f0f..55ce334 100644 --- a/RustRconServerManager.Backend/Controllers/ServerWebhookController.cs +++ b/RustRconServerManager.Backend/Controllers/ServerWebhookController.cs @@ -336,7 +336,7 @@ public async Task TestWebhook([FromBody] ServerWebhooks_TestWebho _logger.LogInformation("TestWebhook: EventType={EventType}, WebhookUrl={WebhookUrl}", request.EventType, - string.IsNullOrWhiteSpace(request.WebhookUrl) ? "(empty)" : request.WebhookUrl.Substring(0, Math.Min(50, request.WebhookUrl.Length))); + string.IsNullOrWhiteSpace(request.WebhookUrl) ? "(empty)" : LogSanitizer.Sanitize(request.WebhookUrl.Substring(0, Math.Min(50, request.WebhookUrl.Length)))); ApplicationUser user = await User.GetUser(_dbContext); _logger.LogInformation("TestWebhook: User retrieved: {UserId}", user.Id); @@ -380,15 +380,19 @@ public async Task TestWebhook([FromBody] ServerWebhooks_TestWebho _logger.LogInformation("TestWebhook: Validating URL format"); if (!Uri.TryCreate(request.WebhookUrl, UriKind.Absolute, out var uri)) { - _logger.LogWarning("TestWebhook: Invalid URL format: {WebhookUrl}", request.WebhookUrl); + _logger.LogWarning("TestWebhook: Invalid URL format: {WebhookUrl}", LogSanitizer.Sanitize(request.WebhookUrl)); return BadRequest("Invalid webhook URL format"); } _logger.LogInformation("TestWebhook: Checking if URL is Discord webhook"); - if (!request.WebhookUrl.Contains("discord.com/api/webhooks/", StringComparison.OrdinalIgnoreCase) && - !request.WebhookUrl.Contains("discordapp.com/api/webhooks/", StringComparison.OrdinalIgnoreCase)) + bool isDiscordHost = uri.Host.Equals("discord.com", StringComparison.OrdinalIgnoreCase) || + uri.Host.Equals("discordapp.com", StringComparison.OrdinalIgnoreCase) || + uri.Host.EndsWith(".discord.com", StringComparison.OrdinalIgnoreCase) || + uri.Host.EndsWith(".discordapp.com", StringComparison.OrdinalIgnoreCase); + + if (!isDiscordHost || !uri.AbsolutePath.StartsWith("/api/webhooks/", StringComparison.OrdinalIgnoreCase)) { - _logger.LogWarning("TestWebhook: URL is not a Discord webhook: {WebhookUrl}", request.WebhookUrl); + _logger.LogWarning("TestWebhook: URL is not a Discord webhook: {WebhookUrl}", LogSanitizer.Sanitize(request.WebhookUrl)); return BadRequest("URL must be a valid Discord webhook URL (discord.com or discordapp.com)"); } diff --git a/RustRconServerManager.Backend/Controllers/StatsController.cs b/RustRconServerManager.Backend/Controllers/StatsController.cs index 914ab29..091f5dd 100644 --- a/RustRconServerManager.Backend/Controllers/StatsController.cs +++ b/RustRconServerManager.Backend/Controllers/StatsController.cs @@ -104,7 +104,8 @@ public async Task GetStats(string statType, string timeRange) // Combine aggregated and recent stats stats.AddRange(recentStats); - _logger.LogInformation($"Retrieved {stats.Count} stats records for server {serverId}, stat type '{statType}', time range '{timeRange}'"); + _logger.LogInformation("Retrieved {StatsCount} stats records for server {ServerId}, stat type '{StatType}', time range '{TimeRange}'", + stats.Count, serverId, LogSanitizer.Sanitize(statType), LogSanitizer.Sanitize(timeRange)); // For player count, round to whole numbers; for other stats, use decimals bool isPlayerCount = statType.ToLower() == "players"; @@ -238,7 +239,7 @@ public async Task GetStats(string statType, string timeRange) } catch (Exception ex) { - _logger.LogError(ex, "Error getting stats for {StatType} over {TimeRange}", statType, timeRange); + _logger.LogError(ex, "Error getting stats for {StatType} over {TimeRange}", LogSanitizer.Sanitize(statType), LogSanitizer.Sanitize(timeRange)); return StatusCode(500, ApiErrorHelper.FormatError("Error getting stats", ex)); } } diff --git a/RustRconServerManager.Backend/Controllers/TriggersController.cs b/RustRconServerManager.Backend/Controllers/TriggersController.cs index b0dad3a..b5a11d7 100644 --- a/RustRconServerManager.Backend/Controllers/TriggersController.cs +++ b/RustRconServerManager.Backend/Controllers/TriggersController.cs @@ -4,6 +4,7 @@ using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; using RustRconServerManager.Backend.Models; +using System.Security.Claims; namespace RustRconServerManager.Backend.Controllers { @@ -74,7 +75,7 @@ public async Task> GetTrigger(int id) // Verify user has access to this server if (!await User.HasServerAccess(_dbContext, trigger.RconServerId)) { - _logger.LogWarning($"User {User.GetEmail()} attempted to access trigger {id} for server {trigger.RconServerId} without authorization"); + _logger.LogWarning("User {UserId} attempted to access trigger {TriggerId} for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, trigger.RconServerId); return Forbid(); } @@ -82,7 +83,7 @@ public async Task> GetTrigger(int id) } catch (Exception ex) { - _logger.LogError(ex, $"Error getting trigger {id}"); + _logger.LogError(ex, "Error getting trigger {TriggerId}", id); return StatusCode(500, new { error = "Failed to retrieve trigger" }); } } @@ -106,7 +107,7 @@ public async Task> CreateTrigger([FromBody] CreateTriggerD // Verify user has access to this server if (!await User.HasServerAccess(_dbContext, serverId)) { - _logger.LogWarning($"User {User.GetEmail()} attempted to create trigger for server {serverId} without authorization"); + _logger.LogWarning("User {UserId} attempted to create trigger for server {ServerId} without authorization", user.Id, serverId); return Forbid(); } @@ -144,7 +145,7 @@ public async Task> CreateTrigger([FromBody] CreateTriggerD _dbContext.Triggers.Add(trigger); await _dbContext.SaveChangesAsync(); - _logger.LogInformation($"User {User.GetEmail()} created trigger {trigger.Id} for server {serverId}"); + _logger.LogInformation("User {UserId} created trigger {TriggerId} for server {ServerId}", user.Id, trigger.Id, serverId); return CreatedAtAction(nameof(GetTrigger), new { id = trigger.Id }, trigger); } @@ -170,7 +171,7 @@ public async Task> UpdateTrigger(int id, [FromBody] Create // Verify user has access to this server if (!await User.HasServerAccess(_dbContext, existing.RconServerId)) { - _logger.LogWarning($"User {User.GetEmail()} attempted to update trigger {id} for server {existing.RconServerId} without authorization"); + _logger.LogWarning("User {UserId} attempted to update trigger {TriggerId} for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, existing.RconServerId); return Forbid(); } @@ -202,13 +203,13 @@ public async Task> UpdateTrigger(int id, [FromBody] Create await _dbContext.SaveChangesAsync(); - _logger.LogInformation($"User {User.GetEmail()} updated trigger {id}"); + _logger.LogInformation("User {UserId} updated trigger {TriggerId}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id); return Ok(existing); } catch (Exception ex) { - _logger.LogError(ex, $"Error updating trigger {id}"); + _logger.LogError(ex, "Error updating trigger {TriggerId}", id); return StatusCode(500, new { error = "Failed to update trigger" }); } } @@ -228,20 +229,20 @@ public async Task DeleteTrigger(int id) // Verify user has access to this server if (!await User.HasServerAccess(_dbContext, trigger.RconServerId)) { - _logger.LogWarning($"User {User.GetEmail()} attempted to delete trigger {id} for server {trigger.RconServerId} without authorization"); + _logger.LogWarning("User {UserId} attempted to delete trigger {TriggerId} for server {ServerId} without authorization", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, trigger.RconServerId); return Forbid(); } _dbContext.Triggers.Remove(trigger); await _dbContext.SaveChangesAsync(); - _logger.LogInformation($"User {User.GetEmail()} deleted trigger {id}"); + _logger.LogInformation("User {UserId} deleted trigger {TriggerId}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id); return NoContent(); } catch (Exception ex) { - _logger.LogError(ex, $"Error deleting trigger {id}"); + _logger.LogError(ex, "Error deleting trigger {TriggerId}", id); return StatusCode(500, new { error = "Failed to delete trigger" }); } } diff --git a/RustRconServerManager.Backend/Helpers/LogSanitizer.cs b/RustRconServerManager.Backend/Helpers/LogSanitizer.cs new file mode 100644 index 0000000..9596140 --- /dev/null +++ b/RustRconServerManager.Backend/Helpers/LogSanitizer.cs @@ -0,0 +1,21 @@ +namespace RustRconServerManager.Backend.Helpers +{ + /// + /// Strips CR/LF characters from user-controlled string values before they reach a logger, + /// so an attacker cannot inject fake newline-delimited log entries (log forging / CWE-117). + /// Structured logging placeholders alone do not prevent this: the substituted value is still + /// rendered verbatim into the formatted text line by the default log providers. + /// + public static class LogSanitizer + { + public static string? Sanitize(string? input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + return input.Replace("\r", "").Replace("\n", ""); + } + } +} diff --git a/RustRconServerManager.Backend/Services/AuditLogService.cs b/RustRconServerManager.Backend/Services/AuditLogService.cs index bdf4710..bfbb27e 100644 --- a/RustRconServerManager.Backend/Services/AuditLogService.cs +++ b/RustRconServerManager.Backend/Services/AuditLogService.cs @@ -1,4 +1,5 @@ using RustRconServerManager.Backend.Database; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; namespace RustRconServerManager.Backend.Services; @@ -46,7 +47,7 @@ public async Task LogAsync(ApplicationUser user, int? serverId, string action, s catch (Exception ex) { // Audit logging must never break the actual action it's recording. - _logger.LogError(ex, "[AuditLogService] Failed to write audit log entry for action {Action}", action); + _logger.LogError(ex, "[AuditLogService] Failed to write audit log entry for action {Action}", LogSanitizer.Sanitize(action)); } } } diff --git a/RustRconServerManager.Backend/Services/DiscordWebhookService.cs b/RustRconServerManager.Backend/Services/DiscordWebhookService.cs index bed7d89..20c28c6 100644 --- a/RustRconServerManager.Backend/Services/DiscordWebhookService.cs +++ b/RustRconServerManager.Backend/Services/DiscordWebhookService.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using RustRconServerManager.Backend.Database; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Interfaces; using RustRconServerManager.Shared.ServerWebhooks; using System.Text; @@ -84,7 +85,7 @@ public async Task SendPlayerConnectAsync(int serverId, string playerName, string } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending player connect webhook for {playerName}"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player connect webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); } } @@ -146,7 +147,7 @@ public async Task SendPlayerDisconnectAsync(int serverId, string playerName, str } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending player disconnect webhook for {playerName}"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player disconnect webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); } } @@ -215,7 +216,7 @@ public async Task SendPlayerKillAsync(int serverId, string killerName, string ki } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending player kill webhook"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player kill webhook", serverId); } } @@ -283,7 +284,7 @@ public async Task SendPlayerBanAsync(int serverId, string playerName, string ste } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending player ban webhook for {playerName}"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player ban webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); } } @@ -347,7 +348,7 @@ public async Task SendPlayerKickAsync(int serverId, string playerName, string st } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending player kick webhook for {playerName}"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player kick webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); } } @@ -418,7 +419,7 @@ public async Task SendPlayerReportAsync(int serverId, string reporterName, strin } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending player report webhook"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player report webhook", serverId); } } @@ -481,7 +482,7 @@ public async Task SendServerOfflineAsync(int serverId, string serverName) } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending server offline webhook for {serverName}"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending server offline webhook for {ServerName}", serverId, LogSanitizer.Sanitize(serverName)); } } @@ -567,7 +568,7 @@ public async Task SendServerOnlineAsync(int serverId, string serverName, int dow } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending server online webhook for {serverName}"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending server online webhook for {ServerName}", serverId, LogSanitizer.Sanitize(serverName)); } } @@ -629,7 +630,7 @@ public async Task SendServerProtectionAsync(int serverId, string playerName, str } catch (Exception ex) { - _logger.LogError(ex, $"[SERVER {serverId}] Error sending server protection webhook for {playerName}"); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending server protection webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); } } @@ -951,7 +952,7 @@ public async Task SendTestWebhookAsync(int serverId, string webhookUrl, WebhookE } catch (Exception ex) { - _logger.LogError(ex, $"Error sending test webhook for {eventType}"); + _logger.LogError(ex, "Error sending test webhook for {EventType}", eventType); throw; } } @@ -968,7 +969,7 @@ public async Task SendTestWebhookAsync(int serverId, string webhookUrl, WebhookE } catch (Exception ex) { - _logger.LogError(ex, $"Error loading webhook settings for server {serverId}"); + _logger.LogError(ex, "Error loading webhook settings for server {ServerId}", serverId); return null; } } @@ -985,7 +986,7 @@ private async Task GetServerNameAsync(int serverId) } catch (Exception ex) { - _logger.LogError(ex, $"Error loading server name for server {serverId}"); + _logger.LogError(ex, "Error loading server name for server {ServerId}", serverId); return $"Server {serverId}"; } } @@ -1001,17 +1002,17 @@ private async Task SendWebhookAsync(string webhookUrl, object payload, string ev if (response.IsSuccessStatusCode) { - _logger.LogInformation($"Successfully sent {eventName} webhook"); + _logger.LogInformation("Successfully sent {EventName} webhook", eventName); } else { var responseBody = await response.Content.ReadAsStringAsync(); - _logger.LogWarning($"Failed to send {eventName} webhook. Status: {response.StatusCode}, Response: {responseBody}"); + _logger.LogWarning("Failed to send {EventName} webhook. Status: {StatusCode}, Response: {ResponseBody}", eventName, response.StatusCode, LogSanitizer.Sanitize(responseBody)); } } catch (Exception ex) { - _logger.LogError(ex, $"Error sending {eventName} webhook to {webhookUrl}"); + _logger.LogError(ex, "Error sending {EventName} webhook to {WebhookUrl}", eventName, LogSanitizer.Sanitize(webhookUrl)); } } @@ -1069,7 +1070,7 @@ private object ParseCustomContent(string customContent, Dictionary SendPasswordRecoveryEmailAsync(string toEmail, string co var host = _configuration["Smtp:Host"]; if (string.IsNullOrWhiteSpace(host)) { - _logger.LogWarning("SMTP is not configured (Smtp:Host is empty) - password recovery email was not sent to {Email}", toEmail); + _logger.LogWarning("SMTP is not configured (Smtp:Host is empty) - password recovery email was not sent to {MaskedEmail}", LogSanitizer.Sanitize(MaskEmail(toEmail))); return false; } @@ -53,8 +54,22 @@ public async Task SendPasswordRecoveryEmailAsync(string toEmail, string co } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to send password recovery email to {Email}", toEmail); + _logger.LogWarning(ex, "Failed to send password recovery email to {MaskedEmail}", LogSanitizer.Sanitize(MaskEmail(toEmail))); return false; } } + + // Logs a partially redacted form of the address (e.g. "j***@example.com") so failures + // remain diagnosable without writing the full recipient email (PII) to the log sink. + private static string MaskEmail(string email) + { + if (string.IsNullOrEmpty(email)) + return "(empty)"; + + var atIndex = email.IndexOf('@'); + if (atIndex <= 0) + return "***"; + + return $"{email[0]}***{email.Substring(atIndex)}"; + } } diff --git a/RustRconServerManager.Backend/Services/MapStorageService.cs b/RustRconServerManager.Backend/Services/MapStorageService.cs index f9fbbc3..15ebf54 100644 --- a/RustRconServerManager.Backend/Services/MapStorageService.cs +++ b/RustRconServerManager.Backend/Services/MapStorageService.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; +using RustRconServerManager.Backend.Helpers; namespace RustRconServerManager.Backend.Services { @@ -188,11 +189,26 @@ private async Task RestoreServerImagesFromDatabase(CancellationToken cancellatio /// /// Gets the file path for a server's custom image. /// Path format: Storage/public/{instanceHash}/{serverId}/{filename} + /// Resolves the path and verifies it is still contained within the public images + /// base directory, since instanceHash is attacker-controlled on the anonymous + /// image-serving endpoint and naive character blacklisting isn't safe here + /// (legitimate hashes are Base64 and may contain '/' and '+'). /// public string GetServerImageFilePath(string instanceHash, int serverId, string filename) { - var directory = Path.Combine(_environment.ContentRootPath, "Storage", "public", instanceHash, serverId.ToString()); - return Path.Combine(directory, filename); + var baseDirectory = Path.Combine(_environment.ContentRootPath, "Storage", "public"); + var directory = Path.Combine(baseDirectory, instanceHash, serverId.ToString()); + var filePath = Path.Combine(directory, filename); + + var fullBase = Path.GetFullPath(baseDirectory) + Path.DirectorySeparatorChar; + var fullPath = Path.GetFullPath(filePath); + + if (!fullPath.StartsWith(fullBase, StringComparison.Ordinal)) + { + throw new ArgumentException("Invalid instanceHash, serverId, or filename: resolved path escapes the public images directory."); + } + + return fullPath; } /// @@ -200,14 +216,13 @@ public string GetServerImageFilePath(string instanceHash, int serverId, string f /// public async Task SaveServerImageToDisk(string instanceHash, int serverId, string filename, byte[] imageData) { - var directory = Path.Combine(_environment.ContentRootPath, "Storage", "public", instanceHash, serverId.ToString()); - Directory.CreateDirectory(directory); + var filePath = GetServerImageFilePath(instanceHash, serverId, filename); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); - var filePath = Path.Combine(directory, filename); await File.WriteAllBytesAsync(filePath, imageData); _logger.LogInformation("MapStorageService: Saved server image {Filename} for server {ServerId} to disk ({Size} bytes)", - filename, serverId, imageData.Length); + LogSanitizer.Sanitize(filename), serverId, imageData.Length); } /// @@ -220,7 +235,7 @@ public void DeleteServerImageFromDisk(string instanceHash, int serverId, string { File.Delete(filePath); _logger.LogInformation("MapStorageService: Deleted server image {Filename} for server {ServerId}", - filename, serverId); + LogSanitizer.Sanitize(filename), serverId); } } } diff --git a/RustRconServerManager.Backend/Services/PluginVersionCheckService.cs b/RustRconServerManager.Backend/Services/PluginVersionCheckService.cs index c8cc327..9696c6a 100644 --- a/RustRconServerManager.Backend/Services/PluginVersionCheckService.cs +++ b/RustRconServerManager.Backend/Services/PluginVersionCheckService.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using RustRconServerManager.Backend.Database; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; using RustRconServerManager.Shared.PluginVersionCheck; @@ -45,23 +46,23 @@ public PluginVersionCheckService( if (pluginSource == null) { - _logger.LogInformation($"[NO SOURCE] Plugin {pluginName} has no source configured for server {serverId} - skipping version check"); + _logger.LogInformation("[NO SOURCE] Plugin {PluginName} has no source configured for server {ServerId} - skipping version check", LogSanitizer.Sanitize(pluginName), serverId); return null; } if (pluginSource.Source == PluginSource.Custom) { - _logger.LogInformation($"[CUSTOM PLUGIN] Plugin {pluginName} is marked as custom for server {serverId} - skipping version check"); + _logger.LogInformation("[CUSTOM PLUGIN] Plugin {PluginName} is marked as custom for server {ServerId} - skipping version check", LogSanitizer.Sanitize(pluginName), serverId); return null; } - _logger.LogInformation($"[SOURCE CHECK] Plugin {pluginName} configured with source {pluginSource.Source} for server {serverId}"); + _logger.LogInformation("[SOURCE CHECK] Plugin {PluginName} configured with source {Source} for server {ServerId}", LogSanitizer.Sanitize(pluginName), pluginSource.Source, serverId); // Step 1: local cache var cached = await GetFromCacheAsync(pluginName); if (cached != null) { - _logger.LogInformation("[CACHE HIT] Plugin {PluginName} found in local cache", pluginName); + _logger.LogInformation("[CACHE HIT] Plugin {PluginName} found in local cache", LogSanitizer.Sanitize(pluginName)); return new PluginVersionCheckResult { PluginName = pluginName, @@ -73,7 +74,7 @@ public PluginVersionCheckService( }; } - _logger.LogInformation($"[CACHE MISS] Plugin {pluginName} not in local cache, checking {pluginSource.Source} API..."); + _logger.LogInformation("[CACHE MISS] Plugin {PluginName} not in local cache, checking {Source} API...", LogSanitizer.Sanitize(pluginName), pluginSource.Source); // Step 2: configured source PluginVersionCheckResult? result = null; @@ -106,7 +107,7 @@ public PluginVersionCheckService( return result; } - _logger.LogWarning($"[NOT FOUND] Plugin {pluginName} not found on {pluginSource.Source}"); + _logger.LogWarning("[NOT FOUND] Plugin {PluginName} not found on {Source}", LogSanitizer.Sanitize(pluginName), pluginSource.Source); return new PluginVersionCheckResult { PluginName = pluginName, @@ -120,7 +121,7 @@ public PluginVersionCheckService( } catch (Exception ex) { - _logger.LogError(ex, $"Error checking version for plugin {pluginName}"); + _logger.LogError(ex, "Error checking version for plugin {PluginName}", LogSanitizer.Sanitize(pluginName)); return new PluginVersionCheckResult { PluginName = pluginName, @@ -234,7 +235,7 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa existing.ExpiresAt = expiresAt; existing.UmodRateLimitRemaining = umodRateLimitRemaining; existing.UmodRateLimitTotal = umodRateLimitTotal; - _logger.LogInformation($"[CACHE UPDATE] {result.PluginName} (expires {expiresAt:O})"); + _logger.LogInformation("[CACHE UPDATE] {PluginName} (expires {ExpiresAt:O})", LogSanitizer.Sanitize(result.PluginName), expiresAt); } else { @@ -249,14 +250,14 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa UmodRateLimitRemaining = umodRateLimitRemaining, UmodRateLimitTotal = umodRateLimitTotal }); - _logger.LogInformation($"[CACHE SAVE] {result.PluginName} (expires {expiresAt:O})"); + _logger.LogInformation("[CACHE SAVE] {PluginName} (expires {ExpiresAt:O})", LogSanitizer.Sanitize(result.PluginName), expiresAt); } await _dbContext.SaveChangesAsync(); } catch (Exception ex) { - _logger.LogError(ex, $"Error saving plugin {result.PluginName} to cache"); + _logger.LogError(ex, "Error saving plugin {PluginName} to cache", LogSanitizer.Sanitize(result.PluginName)); } } @@ -265,14 +266,14 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa try { var url = $"https://www.codefling.com/db/?category=all&filename={Uri.EscapeDataString(fileName)}"; - _logger.LogInformation($"[CODEFLING] Checking filename '{fileName}' -> URL: {url}"); + _logger.LogInformation("[CODEFLING] Checking filename '{FileName}' -> URL: {Url}", LogSanitizer.Sanitize(fileName), LogSanitizer.Sanitize(url)); var response = await _httpClient.GetAsync(url); - _logger.LogInformation($"[CODEFLING] Response for '{fileName}': StatusCode={response.StatusCode}"); + _logger.LogInformation("[CODEFLING] Response for '{FileName}': StatusCode={StatusCode}", LogSanitizer.Sanitize(fileName), response.StatusCode); if (!response.IsSuccessStatusCode) { - _logger.LogWarning($"[CODEFLING] Failed to fetch '{fileName}': {response.StatusCode}"); + _logger.LogWarning("[CODEFLING] Failed to fetch '{FileName}': {StatusCode}", LogSanitizer.Sanitize(fileName), response.StatusCode); return null; } @@ -281,15 +282,15 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa var plugin = plugins?.FirstOrDefault(); if (plugin != null) - _logger.LogInformation($"[CODEFLING] Found plugin '{plugin.Title}' v{plugin.Version}"); + _logger.LogInformation("[CODEFLING] Found plugin '{Title}' v{Version}", LogSanitizer.Sanitize(plugin.Title), LogSanitizer.Sanitize(plugin.Version)); else - _logger.LogInformation($"[CODEFLING] No plugin found for '{fileName}'"); + _logger.LogInformation("[CODEFLING] No plugin found for '{FileName}'", LogSanitizer.Sanitize(fileName)); return plugin; } catch (Exception ex) { - _logger.LogWarning(ex, $"[CODEFLING] Error checking {fileName}"); + _logger.LogWarning(ex, "[CODEFLING] Error checking {FileName}", LogSanitizer.Sanitize(fileName)); return null; } } @@ -322,9 +323,9 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa if (rateLimitRemaining.HasValue && rateLimitTotal.HasValue) { - _logger.LogInformation($"[UMOD RATE LIMIT] {rateLimitRemaining}/{rateLimitTotal} requests remaining"); + _logger.LogInformation("[UMOD RATE LIMIT] {RateLimitRemaining}/{RateLimitTotal} requests remaining", rateLimitRemaining, rateLimitTotal); if (rateLimitRemaining.Value < 5) - _logger.LogWarning($"⚠️ [UMOD RATE LIMIT] Only {rateLimitRemaining} requests remaining!"); + _logger.LogWarning("⚠️ [UMOD RATE LIMIT] Only {RateLimitRemaining} requests remaining!", rateLimitRemaining); } var content = await response.Content.ReadAsStringAsync(); @@ -343,7 +344,7 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa } catch (Exception ex) { - _logger.LogWarning(ex, $"Error checking Umod for {pluginName}"); + _logger.LogWarning(ex, "Error checking Umod for {PluginName}", LogSanitizer.Sanitize(pluginName)); return null; } } diff --git a/RustRconServerManager.Backend/Services/RconBackgroundService.Connections.cs b/RustRconServerManager.Backend/Services/RconBackgroundService.Connections.cs index 947a70c..3dc13fe 100644 --- a/RustRconServerManager.Backend/Services/RconBackgroundService.Connections.cs +++ b/RustRconServerManager.Backend/Services/RconBackgroundService.Connections.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.SignalR; using RustRconServerManager.Backend.Database; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.SignalRHubs; using RustRconServerManager.Backend.Models; using Xenne.RCON; @@ -32,13 +33,13 @@ private async Task StartConnectionWithServer(RconServer server) client.OnMessageReceived += async (sender, args) => { - _logger.LogInformation("[Server {0}] Message: {1}", args.ServerId, args.Message); + _logger.LogInformation("[Server {ServerId}] Message: {Message}", args.ServerId, LogSanitizer.Sanitize(args.Message)); await ServerMessageReceived(args.ServerId, args.Message); }; client.OnCommandAnswerReceived += async (sender, args) => { - _logger.LogDebug("[Server {0}] Answer: {1}", args.ServerId, args.Message); + _logger.LogDebug("[Server {ServerId}] Answer: {Message}", args.ServerId, LogSanitizer.Sanitize(args.Message)); await ServerCommandAnswerReceived(args.ServerId, args.Message, args.Command, args.Purpose); }; @@ -50,19 +51,19 @@ private async Task StartConnectionWithServer(RconServer server) client.OnChatMessageReceived += async (sender, args) => { - _logger.LogWarning("Global chat received: " + args.ChatMessage); + _logger.LogWarning("Global chat received: {ChatMessage}", LogSanitizer.Sanitize(args.ChatMessage)); await OnChatReceived(args.ServerId, args.ChatMessage, args.PlayerId, args.PlayerName, args.Channel.ToString()); }; client.OnPlayerKill += async (sender, args) => { - _logger.LogWarning("Player killed: " + args.KillerName + " killed " + args.VictimName); + _logger.LogWarning("Player killed: {KillerName} killed {VictimName}", LogSanitizer.Sanitize(args.KillerName), LogSanitizer.Sanitize(args.VictimName)); await PlayerKilled(args.ServerId, args.KillerName, args.KillerId, args.VictimName, args.VictimId, args.Position); }; client.OnPlayerConnected += async (sender, args) => { - _logger.LogWarning("Player connected: " + args.PlayerName); + _logger.LogWarning("Player connected: {PlayerName}", LogSanitizer.Sanitize(args.PlayerName)); // Note: args.PlayerId contains the player NAME, args.PlayerName contains the SteamId await OnPlayerConnectedAsync(args.ServerId, args.PlayerName, args.PlayerId, args.PlayerEndpoint); }; @@ -70,14 +71,14 @@ private async Task StartConnectionWithServer(RconServer server) client.OnPlayerDisconnected += async (sender, args) => { _logger.LogInformation("Player disconnected: {PlayerName} ({PlayerId}) - Reason: {Reason}", - args.PlayerName, args.PlayerId, args.Reason); + LogSanitizer.Sanitize(args.PlayerName), args.PlayerId, LogSanitizer.Sanitize(args.Reason)); await OnPlayerDisconnectedAsync(args.ServerId, args.PlayerId, args.PlayerName, args.Reason); }; client.OnPlayerReported += async (sender, args) => { _logger.LogInformation("Player reported: {Reporter} reported {Reported} for {Type}", - args.ReporterName, args.ReportedName, args.Type); + LogSanitizer.Sanitize(args.ReporterName), LogSanitizer.Sanitize(args.ReportedName), LogSanitizer.Sanitize(args.Type)); await OnPlayerReportedAsync(args.ServerId, args.ReporterName, args.ReporterId, args.ReportedName, args.ReportedId, args.Subject, args.Message, args.Type); }; @@ -123,7 +124,7 @@ private async Task ServerMessageReceived(int serverId, string message) using var scope = _scopeFactory.CreateScope(); AppDbContext dbContext = scope.ServiceProvider.GetRequiredService(); - _logger.LogInformation("[RconBackgroundService] Message from Server {ServerId}: {Message}", serverId, message); + _logger.LogInformation("[RconBackgroundService] Message from Server {ServerId}: {Message}", serverId, LogSanitizer.Sanitize(message)); RconLogEntry logEntry = new RconLogEntry(); logEntry.CreatedAt = DateTime.UtcNow; From 5e8337527107bd6db41e7eeea897ce929efb50d3 Mon Sep 17 00:00:00 2001 From: Xenne <144433308+Xenne93@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:59:18 +0200 Subject: [PATCH 2/4] test: inline Replace() sanitizer instead of helper method to validate CodeQL barrier recognition --- RustRconServerManager.Backend/Services/AuditLogService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RustRconServerManager.Backend/Services/AuditLogService.cs b/RustRconServerManager.Backend/Services/AuditLogService.cs index bfbb27e..11c6d51 100644 --- a/RustRconServerManager.Backend/Services/AuditLogService.cs +++ b/RustRconServerManager.Backend/Services/AuditLogService.cs @@ -1,5 +1,4 @@ using RustRconServerManager.Backend.Database; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; namespace RustRconServerManager.Backend.Services; @@ -47,7 +46,7 @@ public async Task LogAsync(ApplicationUser user, int? serverId, string action, s catch (Exception ex) { // Audit logging must never break the actual action it's recording. - _logger.LogError(ex, "[AuditLogService] Failed to write audit log entry for action {Action}", LogSanitizer.Sanitize(action)); + _logger.LogError(ex, "[AuditLogService] Failed to write audit log entry for action {Action}", action.Replace("\r", "").Replace("\n", "")); } } } From 374ba203c207f42fca5ae419378769e45457e823 Mon Sep 17 00:00:00 2001 From: Xenne <144433308+Xenne93@users.noreply.github.com> Date: Sun, 16 Aug 2026 13:26:26 +0200 Subject: [PATCH 3/4] Replace LogSanitizer helper with inline .Replace() calls CodeQL's cs/log-forging barrier recognition requires the sanitizing .Replace() call to appear directly in the tainted expression, not routed through a custom static helper method - validated empirically via a live re-scan (helper-based fix cleared only 1/65 alerts, one inlined call site cleared immediately). Converted every remaining call site accordingly and removed the now-dead LogSanitizer helper. --- .../Controllers/DashboardController.cs | 22 ++++----- .../Controllers/ModsController.cs | 26 +++++----- .../Controllers/PanelSettingsController.cs | 3 +- .../PermissionsManagerController.cs | 14 +++--- .../Controllers/PlayerInspectController.cs | 17 ++++--- .../Controllers/PlayerNotesController.cs | 11 ++--- .../Controllers/PresetCommandsController.cs | 3 +- .../Controllers/SchedulerController.cs | 3 +- .../Controllers/ServerWebhookController.cs | 8 ++-- .../Controllers/StatsController.cs | 4 +- .../Helpers/LogSanitizer.cs | 21 --------- .../Services/DiscordWebhookService.cs | 47 +++++++++---------- .../Services/EmailService.cs | 5 +- .../Services/MapStorageService.cs | 4 +- .../Services/PluginVersionCheckService.cs | 43 +++++++++-------- .../RconBackgroundService.Connections.cs | 19 ++++---- 16 files changed, 110 insertions(+), 140 deletions(-) delete mode 100644 RustRconServerManager.Backend/Helpers/LogSanitizer.cs diff --git a/RustRconServerManager.Backend/Controllers/DashboardController.cs b/RustRconServerManager.Backend/Controllers/DashboardController.cs index 4a2cb01..795d0b0 100644 --- a/RustRconServerManager.Backend/Controllers/DashboardController.cs +++ b/RustRconServerManager.Backend/Controllers/DashboardController.cs @@ -352,12 +352,12 @@ public async Task BanPlayer([FromBody] BanPlayerRequest request) { string banCommand = $"banid {request.SteamId} \"{request.PlayerName}\" \"[GLOBAL BAN] {request.Reason}\" {request.DurationHours}"; _rconService.SendRconCommand(banCommand, server.Id); - _logger.LogInformation("Sent global ban command to server {ServerName} (ID: {ServerId}) for player {PlayerName}", LogSanitizer.Sanitize(server.Name), server.Id, LogSanitizer.Sanitize(request.PlayerName)); + _logger.LogInformation("Sent global ban command to server {ServerName} (ID: {ServerId}) for player {PlayerName}", server.Name?.Replace("\r", "").Replace("\n", ""), server.Id, request.PlayerName?.Replace("\r", "").Replace("\n", "")); } catch (Exception ex) { // Log but don't fail if server is offline - ban is still tracked in database - _logger.LogWarning(ex, "Could not send global ban command to server {ServerName}", LogSanitizer.Sanitize(server.Name)); + _logger.LogWarning(ex, "Could not send global ban command to server {ServerName}", server.Name?.Replace("\r", "").Replace("\n", "")); } } } @@ -870,7 +870,7 @@ public async Task ToggleGlobalBan(int banId, [FromBody] ToggleGlo // If toggling OFF a global ban, unban from all servers if (ban.IsGlobalBan && !request.IsGlobalBan) { - _logger.LogInformation("[GLOBAL UNBAN] Toggling off global ban for {SteamId}. Unbanning from all servers...", LogSanitizer.Sanitize(ban.SteamId)); + _logger.LogInformation("[GLOBAL UNBAN] Toggling off global ban for {SteamId}. Unbanning from all servers...", ban.SteamId?.Replace("\r", "").Replace("\n", "")); // Get current user email for audit trail var userEmail = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "System"; @@ -902,7 +902,7 @@ public async Task ToggleGlobalBan(int banId, [FromBody] ToggleGlo { string unbanCommand = $"unban {playerBan.SteamId}"; _rconService.SendRconCommand(unbanCommand, playerBan.ServerId); - _logger.LogInformation("[GLOBAL UNBAN] Sent unban command for {SteamId} on server {ServerId}", LogSanitizer.Sanitize(ban.SteamId), playerBan.ServerId); + _logger.LogInformation("[GLOBAL UNBAN] Sent unban command for {SteamId} on server {ServerId}", ban.SteamId?.Replace("\r", "").Replace("\n", ""), playerBan.ServerId); } catch (Exception ex) { @@ -914,7 +914,7 @@ public async Task ToggleGlobalBan(int banId, [FromBody] ToggleGlo } await dbContext.SaveChangesAsync(); - _logger.LogInformation("[GLOBAL UNBAN] Removed all bans for {SteamId} from database", LogSanitizer.Sanitize(ban.SteamId)); + _logger.LogInformation("[GLOBAL UNBAN] Removed all bans for {SteamId} from database", ban.SteamId?.Replace("\r", "").Replace("\n", "")); return Ok(new { message = $"Player {ban.SteamId} has been unbanned from all servers" }); } @@ -966,7 +966,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques // If this is a global ban, unban from all servers if (ban.IsGlobalBan && ban.ServerId == -1) { - _logger.LogInformation("[GLOBAL UNBAN] Removing global ban for {SteamId}", LogSanitizer.Sanitize(ban.SteamId)); + _logger.LogInformation("[GLOBAL UNBAN] Removing global ban for {SteamId}", ban.SteamId?.Replace("\r", "").Replace("\n", "")); // Find all server-specific bans created by this global ban var relatedServerBans = await dbContext.PlayerBans @@ -994,7 +994,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques { string unbanCommand = $"unban {serverBan.SteamId}"; _rconService.SendRconCommand(unbanCommand, serverBan.ServerId); - _logger.LogInformation("[GLOBAL UNBAN] Sent unban command for {SteamId} on server {ServerId}", LogSanitizer.Sanitize(ban.SteamId), serverBan.ServerId); + _logger.LogInformation("[GLOBAL UNBAN] Sent unban command for {SteamId} on server {ServerId}", ban.SteamId?.Replace("\r", "").Replace("\n", ""), serverBan.ServerId); } catch (Exception ex) { @@ -1057,7 +1057,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques { string unbanCommand = $"unban {ban.SteamId}"; _rconService.SendRconCommand(unbanCommand, ban.ServerId); - _logger.LogInformation("[UNBAN] Sent unban command for SteamID {SteamId} on server {ServerId}", LogSanitizer.Sanitize(ban.SteamId), ban.ServerId); + _logger.LogInformation("[UNBAN] Sent unban command for SteamID {SteamId} on server {ServerId}", ban.SteamId?.Replace("\r", "").Replace("\n", ""), ban.ServerId); } catch (Exception ex) { @@ -1068,7 +1068,7 @@ public async Task DeleteBan(int banId, [FromBody] DeleteBanReques // Remove the ban record from database dbContext.PlayerBans.Remove(ban); await dbContext.SaveChangesAsync(); - _logger.LogInformation("[UNBAN] Removed ban record for SteamID {SteamId} from database", LogSanitizer.Sanitize(ban.SteamId)); + _logger.LogInformation("[UNBAN] Removed ban record for SteamID {SteamId} from database", ban.SteamId?.Replace("\r", "").Replace("\n", "")); return Ok(new { message = "Player unbanned successfully on server and database updated" }); } @@ -1342,12 +1342,12 @@ public async Task GiveItem([FromBody] GiveItemRequest request) // Build the RCON command: inventory.giveto var command = $"inventory.giveto {request.SteamId} {request.ShortName} {request.Quantity}"; - _logger.LogInformation("[GIVE ITEM] Executing command: {Command} for player {PlayerName}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(request.PlayerName)); + _logger.LogInformation("[GIVE ITEM] Executing command: {Command} for player {PlayerName}", command?.Replace("\r", "").Replace("\n", ""), request.PlayerName?.Replace("\r", "").Replace("\n", "")); // Execute the RCON command var response = await _rconService.ExecuteCommandWithResponse(command, request.ServerId); - _logger.LogDebug("[GIVE ITEM] Command response: {Response}", LogSanitizer.Sanitize(response)); + _logger.LogDebug("[GIVE ITEM] Command response: {Response}", response?.Replace("\r", "").Replace("\n", "")); return Ok(new { success = true, diff --git a/RustRconServerManager.Backend/Controllers/ModsController.cs b/RustRconServerManager.Backend/Controllers/ModsController.cs index e78b120..ca1ff73 100644 --- a/RustRconServerManager.Backend/Controllers/ModsController.cs +++ b/RustRconServerManager.Backend/Controllers/ModsController.cs @@ -77,7 +77,7 @@ public async Task GetPlugins() // Execute RCON command to get plugins string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", command?.Replace("\r", "").Replace("\n", ""), rconResponse?.Replace("\r", "").Replace("\n", "") ?? "NULL"); if (string.IsNullOrWhiteSpace(rconResponse)) { @@ -182,12 +182,12 @@ public async Task LoadPlugin([FromBody] Mods_ReloadPluginDTO requ ? $"c.load {request.PluginName}" : $"o.load {request.PluginName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", command?.Replace("\r", "").Replace("\n", ""), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", command?.Replace("\r", "").Replace("\n", ""), rconResponse?.Replace("\r", "").Replace("\n", "") ?? "NULL"); return Ok(new { @@ -249,12 +249,12 @@ public async Task ReloadPlugin([FromBody] Mods_ReloadPluginDTO re ? $"c.reload {request.PluginName}" : $"o.reload {request.PluginName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", command?.Replace("\r", "").Replace("\n", ""), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", command?.Replace("\r", "").Replace("\n", ""), rconResponse?.Replace("\r", "").Replace("\n", "") ?? "NULL"); return Ok(new { @@ -316,12 +316,12 @@ public async Task UnloadPlugin([FromBody] Mods_UnloadPluginDTO re ? $"c.unload {request.PluginName}" : $"o.unload {request.PluginName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", command?.Replace("\r", "").Replace("\n", ""), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", command?.Replace("\r", "").Replace("\n", ""), rconResponse?.Replace("\r", "").Replace("\n", "") ?? "NULL"); return Ok(new { @@ -377,12 +377,12 @@ public async Task ReloadAll() ? "c.reload *" : "o.reload *"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", command?.Replace("\r", "").Replace("\n", ""), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", command?.Replace("\r", "").Replace("\n", ""), rconResponse?.Replace("\r", "").Replace("\n", "") ?? "NULL"); return Ok(new { @@ -444,7 +444,7 @@ public async Task SetPluginSource([FromBody] Mods_SetPluginSource _dbContext.ServerPluginSources.Remove(existingEntry); await _dbContext.SaveChangesAsync(); - _logger.LogInformation("Removed plugin source for {PluginName} on server {ServerId}", LogSanitizer.Sanitize(request.PluginName), serverId); + _logger.LogInformation("Removed plugin source for {PluginName} on server {ServerId}", request.PluginName?.Replace("\r", "").Replace("\n", ""), serverId); return Ok(new { @@ -468,7 +468,7 @@ public async Task SetPluginSource([FromBody] Mods_SetPluginSource await _dbContext.SaveChangesAsync(); _logger.LogInformation("Updated plugin source for {PluginName} on server {ServerId} to {Source}", - LogSanitizer.Sanitize(request.PluginName), serverId, request.Source.Value); + request.PluginName?.Replace("\r", "").Replace("\n", ""), serverId, request.Source.Value); return Ok(new { @@ -491,7 +491,7 @@ public async Task SetPluginSource([FromBody] Mods_SetPluginSource await _dbContext.SaveChangesAsync(); _logger.LogInformation("Created plugin source for {PluginName} on server {ServerId} as {Source}", - LogSanitizer.Sanitize(request.PluginName), serverId, request.Source.Value); + request.PluginName?.Replace("\r", "").Replace("\n", ""), serverId, request.Source.Value); return Ok(new { @@ -636,7 +636,7 @@ public async Task CheckPluginVersion([FromQuery] string pluginNam } catch (Exception ex) { - _logger.LogError(ex, "Error checking version for plugin {PluginName}", LogSanitizer.Sanitize(pluginName)); + _logger.LogError(ex, "Error checking version for plugin {PluginName}", pluginName?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, ApiErrorHelper.FormatError("Error checking plugin version", ex)); } } diff --git a/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs b/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs index 53af50e..14bc14c 100644 --- a/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs +++ b/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; using RustRconServerManager.Backend.Services; using RustRconServerManager.Shared.Scheduler; @@ -219,7 +218,7 @@ public async Task PurgeData([FromBody] PurgeDataRequestDto reques } _logger.LogInformation("[PanelSettingsController] User {UserId} purged {Count} records from {Category} (older than {Days} days)", - userId, deleted, LogSanitizer.Sanitize(request.Category), request.OlderThanDays); + userId, deleted, request.Category?.Replace("\r", "").Replace("\n", ""), request.OlderThanDays); return Ok(new { deleted, category = request.Category }); } diff --git a/RustRconServerManager.Backend/Controllers/PermissionsManagerController.cs b/RustRconServerManager.Backend/Controllers/PermissionsManagerController.cs index ed664e3..df247c9 100644 --- a/RustRconServerManager.Backend/Controllers/PermissionsManagerController.cs +++ b/RustRconServerManager.Backend/Controllers/PermissionsManagerController.cs @@ -3,9 +3,9 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Interfaces; using RustRconServerManager.Backend.Models; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Shared.PermissionsManager; using System.Text.RegularExpressions; @@ -153,7 +153,7 @@ public async Task GetGroupPlayers([FromQuery] string groupName) // Execute RCON command to get group details string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", command.Replace("\r", "").Replace("\n", ""), rconResponse?.Replace("\r", "").Replace("\n", "") ?? "NULL"); if (string.IsNullOrWhiteSpace(rconResponse)) { @@ -163,7 +163,7 @@ public async Task GetGroupPlayers([FromQuery] string groupName) // Parse the RCON response to extract players var players = ParsePlayersFromGroupResponse(rconResponse, groupName); - _logger.LogInformation("Parsed {Count} players for group {GroupName}", players.Count, LogSanitizer.Sanitize(groupName)); + _logger.LogInformation("Parsed {Count} players for group {GroupName}", players.Count, groupName.Replace("\r", "").Replace("\n", "")); return Ok(players); } @@ -277,12 +277,12 @@ public async Task AddPlayerToGroup([FromBody] PermissionsManager_ ? $"c.usergroup add {request.SteamId} {request.GroupName}" : $"o.usergroup add {request.SteamId} {request.GroupName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", command.Replace("\r", "").Replace("\n", ""), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", command.Replace("\r", "").Replace("\n", ""), rconResponse?.Replace("\r", "").Replace("\n", "") ?? "NULL"); return Ok(new { message = $"Player {request.SteamId} added to group {request.GroupName}", @@ -342,12 +342,12 @@ public async Task RemovePlayerFromGroup([FromBody] PermissionsMan ? $"c.usergroup remove {request.SteamId} {request.GroupName}" : $"o.usergroup remove {request.SteamId} {request.GroupName}"; - _logger.LogInformation("Executing command: {Command} for server {ServerId}", LogSanitizer.Sanitize(command), serverId); + _logger.LogInformation("Executing command: {Command} for server {ServerId}", command.Replace("\r", "").Replace("\n", ""), serverId); // Execute RCON command string? rconResponse = await _rconBackgroundService.ExecuteCommandWithResponse(command, serverId); - _logger.LogInformation("RCON Response for '{Command}': {Response}", LogSanitizer.Sanitize(command), LogSanitizer.Sanitize(rconResponse) ?? "NULL"); + _logger.LogInformation("RCON Response for '{Command}': {Response}", command.Replace("\r", "").Replace("\n", ""), rconResponse?.Replace("\r", "").Replace("\n", "") ?? "NULL"); return Ok(new { message = $"Player {request.SteamId} removed from group {request.GroupName}", diff --git a/RustRconServerManager.Backend/Controllers/PlayerInspectController.cs b/RustRconServerManager.Backend/Controllers/PlayerInspectController.cs index 0db99ab..9a41ea4 100644 --- a/RustRconServerManager.Backend/Controllers/PlayerInspectController.cs +++ b/RustRconServerManager.Backend/Controllers/PlayerInspectController.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Interfaces; using RustRconServerManager.Shared.PlayerInspect; @@ -211,7 +210,7 @@ public async Task GetPlayerData(string steamId) } catch (Exception ex) { - _logger.LogError(ex, "Error fetching player data for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); + _logger.LogError(ex, "Error fetching player data for Steam ID {SteamId}", steamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, new { message = "Internal server error" }); } } @@ -258,7 +257,7 @@ public async Task GetPlayerChatMessages(string steamId, [FromQuer } catch (Exception ex) { - _logger.LogError(ex, "Error fetching chat messages for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); + _logger.LogError(ex, "Error fetching chat messages for Steam ID {SteamId}", steamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, new { message = "Internal server error" }); } } @@ -305,7 +304,7 @@ public async Task SearchPlayerChatMessages(string steamId, [FromQ } catch (Exception ex) { - _logger.LogError(ex, "Error searching chat messages for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); + _logger.LogError(ex, "Error searching chat messages for Steam ID {SteamId}", steamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, new { message = "Internal server error" }); } } @@ -355,7 +354,7 @@ public async Task GetPlayerKills(string steamId, [FromQuery] int } catch (Exception ex) { - _logger.LogError(ex, "Error fetching kills for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); + _logger.LogError(ex, "Error fetching kills for Steam ID {SteamId}", steamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, new { message = "Internal server error" }); } } @@ -405,7 +404,7 @@ public async Task GetPlayerDeaths(string steamId, [FromQuery] int } catch (Exception ex) { - _logger.LogError(ex, "Error fetching deaths for Steam ID {SteamId}", LogSanitizer.Sanitize(steamId)); + _logger.LogError(ex, "Error fetching deaths for Steam ID {SteamId}", steamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, new { message = "Internal server error" }); } } @@ -497,7 +496,7 @@ public async Task ClearPlayerData(string steamId, [FromBody] Clea await dbContext.SaveChangesAsync(); _logger.LogInformation("[PlayerInspect] Cleared data for {SteamId} on server {ServerId}: Chat={Chat}, Kills={Kills}, Deaths={Deaths}, Notes={Notes}, Reports={Reports}, BanHistory={BanHistory}, PlayerDeleted={Deleted}", - LogSanitizer.Sanitize(steamId), request.ServerId, deletedChat, deletedKills, deletedDeaths, deletedNotes, deletedReports, deletedBanHistory, request.DeletePlayer); + steamId?.Replace("\r", "").Replace("\n", ""), request.ServerId, deletedChat, deletedKills, deletedDeaths, deletedNotes, deletedReports, deletedBanHistory, request.DeletePlayer); return Ok(new { @@ -513,7 +512,7 @@ public async Task ClearPlayerData(string steamId, [FromBody] Clea } catch (Exception ex) { - _logger.LogError(ex, "Error clearing player data for {SteamId}", LogSanitizer.Sanitize(steamId)); + _logger.LogError(ex, "Error clearing player data for {SteamId}", steamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, new { message = "Internal server error" }); } } @@ -563,7 +562,7 @@ public async Task GetPlayerInventory(string steamId) } catch (Exception ex) { - _logger.LogError(ex, "Error getting player inventory for {SteamId}", LogSanitizer.Sanitize(steamId)); + _logger.LogError(ex, "Error getting player inventory for {SteamId}", steamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, "An error occurred while fetching the inventory."); } } diff --git a/RustRconServerManager.Backend/Controllers/PlayerNotesController.cs b/RustRconServerManager.Backend/Controllers/PlayerNotesController.cs index d5d3b5f..f15af79 100644 --- a/RustRconServerManager.Backend/Controllers/PlayerNotesController.cs +++ b/RustRconServerManager.Backend/Controllers/PlayerNotesController.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; using RustRconServerManager.Shared.PlayerNotes; using System.Security.Claims; @@ -62,7 +61,7 @@ public async Task> GetPlayerNotes(string st } catch (Exception ex) { - _logger.LogError(ex, "[PlayerNotesController] Error getting notes for player {SteamId}", LogSanitizer.Sanitize(steamId)); + _logger.LogError(ex, "[PlayerNotesController] Error getting notes for player {SteamId}", steamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, new { error = "Error retrieving player notes" }); } } @@ -101,7 +100,7 @@ public async Task> CreatePlayerNote([FromBody] Creat await _dbContext.SaveChangesAsync(); _logger.LogInformation("[PlayerNotesController] User {UserId} created note {NoteId} for player {SteamId} on server {ServerId}", - userId, playerNote.Id, LogSanitizer.Sanitize(request.SteamId), request.ServerId); + userId, playerNote.Id, request.SteamId?.Replace("\r", "").Replace("\n", ""), request.ServerId); var dto = new PlayerNoteDTO { @@ -118,7 +117,7 @@ public async Task> CreatePlayerNote([FromBody] Creat } catch (Exception ex) { - _logger.LogError(ex, "[PlayerNotesController] Error creating note for player {SteamId}", LogSanitizer.Sanitize(request.SteamId)); + _logger.LogError(ex, "[PlayerNotesController] Error creating note for player {SteamId}", request.SteamId?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, new { error = "Error creating player note" }); } } @@ -156,7 +155,7 @@ public async Task> UpdatePlayerNote(int noteId, [Fro var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; _logger.LogInformation("[PlayerNotesController] User {UserId} updated note {NoteId} for player {SteamId}", - userId, noteId, LogSanitizer.Sanitize(playerNote.SteamId)); + userId, noteId, playerNote.SteamId?.Replace("\r", "").Replace("\n", "")); var dto = new PlayerNoteDTO { @@ -203,7 +202,7 @@ public async Task DeletePlayerNote(int noteId) var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; _logger.LogInformation("[PlayerNotesController] User {UserId} deleted note {NoteId} for player {SteamId}", - userId, noteId, LogSanitizer.Sanitize(playerNote.SteamId)); + userId, noteId, playerNote.SteamId?.Replace("\r", "").Replace("\n", "")); return NoContent(); } diff --git a/RustRconServerManager.Backend/Controllers/PresetCommandsController.cs b/RustRconServerManager.Backend/Controllers/PresetCommandsController.cs index 10d778a..93ac276 100644 --- a/RustRconServerManager.Backend/Controllers/PresetCommandsController.cs +++ b/RustRconServerManager.Backend/Controllers/PresetCommandsController.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; using RustRconServerManager.Shared.PresetCommand; using System.Security.Claims; @@ -94,7 +93,7 @@ public async Task> CreatePresetCommand([FromBody] _dbContext.PresetCommands.Add(command); await _dbContext.SaveChangesAsync(); - _logger.LogInformation("[PresetCommandsController] User {UserId} created preset command {PresetId} '{PresetName}'", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, command.Id, LogSanitizer.Sanitize(command.Name)); + _logger.LogInformation("[PresetCommandsController] User {UserId} created preset command {PresetId} '{PresetName}'", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, command.Id, command.Name?.Replace("\r", "").Replace("\n", "")); return CreatedAtAction(nameof(GetPresetCommands), new { serverId = command.RconServerId ?? 0 }, MapToDto(command)); } diff --git a/RustRconServerManager.Backend/Controllers/SchedulerController.cs b/RustRconServerManager.Backend/Controllers/SchedulerController.cs index 8ead8d9..ed60c96 100644 --- a/RustRconServerManager.Backend/Controllers/SchedulerController.cs +++ b/RustRconServerManager.Backend/Controllers/SchedulerController.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Services; using RustRconServerManager.Backend.Models; using RustRconServerManager.Shared.Scheduler; @@ -230,7 +229,7 @@ public async Task ExecuteNow(int id) try { - _logger.LogInformation("[SchedulerController] User {UserId} executing command {CommandId} ({CommandName}) manually: {Command}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, LogSanitizer.Sanitize(command.Name), LogSanitizer.Sanitize(command.Command)); + _logger.LogInformation("[SchedulerController] User {UserId} executing command {CommandId} ({CommandName}) manually: {Command}", User.FindFirst(ClaimTypes.NameIdentifier)?.Value, id, command.Name?.Replace("\r", "").Replace("\n", ""), command.Command?.Replace("\r", "").Replace("\n", "")); await client.SendCommandAsync(command.Command); _logger.LogInformation("[SchedulerController] Command {CommandId} executed successfully", id); diff --git a/RustRconServerManager.Backend/Controllers/ServerWebhookController.cs b/RustRconServerManager.Backend/Controllers/ServerWebhookController.cs index 55ce334..6c47a67 100644 --- a/RustRconServerManager.Backend/Controllers/ServerWebhookController.cs +++ b/RustRconServerManager.Backend/Controllers/ServerWebhookController.cs @@ -3,9 +3,9 @@ using Microsoft.EntityFrameworkCore; using RustRconServerManager.Backend.Database; using RustRconServerManager.Backend.Extensions; +using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Interfaces; using RustRconServerManager.Backend.Models; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Shared.ServerWebhooks; namespace RustRconServerManager.Backend.Controllers; @@ -336,7 +336,7 @@ public async Task TestWebhook([FromBody] ServerWebhooks_TestWebho _logger.LogInformation("TestWebhook: EventType={EventType}, WebhookUrl={WebhookUrl}", request.EventType, - string.IsNullOrWhiteSpace(request.WebhookUrl) ? "(empty)" : LogSanitizer.Sanitize(request.WebhookUrl.Substring(0, Math.Min(50, request.WebhookUrl.Length)))); + string.IsNullOrWhiteSpace(request.WebhookUrl) ? "(empty)" : request.WebhookUrl.Substring(0, Math.Min(50, request.WebhookUrl.Length)).Replace("\r", "").Replace("\n", "")); ApplicationUser user = await User.GetUser(_dbContext); _logger.LogInformation("TestWebhook: User retrieved: {UserId}", user.Id); @@ -380,7 +380,7 @@ public async Task TestWebhook([FromBody] ServerWebhooks_TestWebho _logger.LogInformation("TestWebhook: Validating URL format"); if (!Uri.TryCreate(request.WebhookUrl, UriKind.Absolute, out var uri)) { - _logger.LogWarning("TestWebhook: Invalid URL format: {WebhookUrl}", LogSanitizer.Sanitize(request.WebhookUrl)); + _logger.LogWarning("TestWebhook: Invalid URL format: {WebhookUrl}", request.WebhookUrl?.Replace("\r", "").Replace("\n", "")); return BadRequest("Invalid webhook URL format"); } @@ -392,7 +392,7 @@ public async Task TestWebhook([FromBody] ServerWebhooks_TestWebho if (!isDiscordHost || !uri.AbsolutePath.StartsWith("/api/webhooks/", StringComparison.OrdinalIgnoreCase)) { - _logger.LogWarning("TestWebhook: URL is not a Discord webhook: {WebhookUrl}", LogSanitizer.Sanitize(request.WebhookUrl)); + _logger.LogWarning("TestWebhook: URL is not a Discord webhook: {WebhookUrl}", request.WebhookUrl?.Replace("\r", "").Replace("\n", "")); return BadRequest("URL must be a valid Discord webhook URL (discord.com or discordapp.com)"); } diff --git a/RustRconServerManager.Backend/Controllers/StatsController.cs b/RustRconServerManager.Backend/Controllers/StatsController.cs index 091f5dd..3f315bd 100644 --- a/RustRconServerManager.Backend/Controllers/StatsController.cs +++ b/RustRconServerManager.Backend/Controllers/StatsController.cs @@ -105,7 +105,7 @@ public async Task GetStats(string statType, string timeRange) stats.AddRange(recentStats); _logger.LogInformation("Retrieved {StatsCount} stats records for server {ServerId}, stat type '{StatType}', time range '{TimeRange}'", - stats.Count, serverId, LogSanitizer.Sanitize(statType), LogSanitizer.Sanitize(timeRange)); + stats.Count, serverId, statType?.Replace("\r", "").Replace("\n", ""), timeRange?.Replace("\r", "").Replace("\n", "")); // For player count, round to whole numbers; for other stats, use decimals bool isPlayerCount = statType.ToLower() == "players"; @@ -239,7 +239,7 @@ public async Task GetStats(string statType, string timeRange) } catch (Exception ex) { - _logger.LogError(ex, "Error getting stats for {StatType} over {TimeRange}", LogSanitizer.Sanitize(statType), LogSanitizer.Sanitize(timeRange)); + _logger.LogError(ex, "Error getting stats for {StatType} over {TimeRange}", statType?.Replace("\r", "").Replace("\n", ""), timeRange?.Replace("\r", "").Replace("\n", "")); return StatusCode(500, ApiErrorHelper.FormatError("Error getting stats", ex)); } } diff --git a/RustRconServerManager.Backend/Helpers/LogSanitizer.cs b/RustRconServerManager.Backend/Helpers/LogSanitizer.cs deleted file mode 100644 index 9596140..0000000 --- a/RustRconServerManager.Backend/Helpers/LogSanitizer.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace RustRconServerManager.Backend.Helpers -{ - /// - /// Strips CR/LF characters from user-controlled string values before they reach a logger, - /// so an attacker cannot inject fake newline-delimited log entries (log forging / CWE-117). - /// Structured logging placeholders alone do not prevent this: the substituted value is still - /// rendered verbatim into the formatted text line by the default log providers. - /// - public static class LogSanitizer - { - public static string? Sanitize(string? input) - { - if (string.IsNullOrEmpty(input)) - { - return input; - } - - return input.Replace("\r", "").Replace("\n", ""); - } - } -} diff --git a/RustRconServerManager.Backend/Services/DiscordWebhookService.cs b/RustRconServerManager.Backend/Services/DiscordWebhookService.cs index 20c28c6..22ced84 100644 --- a/RustRconServerManager.Backend/Services/DiscordWebhookService.cs +++ b/RustRconServerManager.Backend/Services/DiscordWebhookService.cs @@ -1,7 +1,6 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using RustRconServerManager.Backend.Database; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Interfaces; using RustRconServerManager.Shared.ServerWebhooks; using System.Text; @@ -85,7 +84,7 @@ public async Task SendPlayerConnectAsync(int serverId, string playerName, string } catch (Exception ex) { - _logger.LogError(ex, "[SERVER {ServerId}] Error sending player connect webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player connect webhook for {PlayerName}", serverId, playerName?.Replace("\r", "").Replace("\n", "")); } } @@ -147,7 +146,7 @@ public async Task SendPlayerDisconnectAsync(int serverId, string playerName, str } catch (Exception ex) { - _logger.LogError(ex, "[SERVER {ServerId}] Error sending player disconnect webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player disconnect webhook for {PlayerName}", serverId, playerName?.Replace("\r", "").Replace("\n", "")); } } @@ -284,7 +283,7 @@ public async Task SendPlayerBanAsync(int serverId, string playerName, string ste } catch (Exception ex) { - _logger.LogError(ex, "[SERVER {ServerId}] Error sending player ban webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player ban webhook for {PlayerName}", serverId, playerName?.Replace("\r", "").Replace("\n", "")); } } @@ -348,7 +347,7 @@ public async Task SendPlayerKickAsync(int serverId, string playerName, string st } catch (Exception ex) { - _logger.LogError(ex, "[SERVER {ServerId}] Error sending player kick webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending player kick webhook for {PlayerName}", serverId, playerName?.Replace("\r", "").Replace("\n", "")); } } @@ -444,7 +443,7 @@ public async Task SendServerOfflineAsync(int serverId, string serverName) // Text message format - use custom text content if provided, otherwise default var textMessage = string.IsNullOrWhiteSpace(settings.ServerOfflineTextContent) - ? $"🔴 **{serverName}** is offline!" + ? $"🔴 **{serverName}** is offline!" : ReplaceVariables(settings.ServerOfflineTextContent, variables); // Default embed format @@ -454,13 +453,13 @@ public async Task SendServerOfflineAsync(int serverId, string serverName) { new { - title = "🔴 Server Offline", + title = "🔴 Server Offline", description = $"**{serverName}** is no longer reachable", color = 15158332, // Red fields = new[] { new { name = "Server", value = serverName, inline = true }, - new { name = "Status", value = "❌ Offline", inline = true }, + new { name = "Status", value = "❌ Offline", inline = true }, new { name = "Time", value = $"", inline = false } }, footer = new { text = "Server Status Monitor" }, @@ -482,7 +481,7 @@ public async Task SendServerOfflineAsync(int serverId, string serverName) } catch (Exception ex) { - _logger.LogError(ex, "[SERVER {ServerId}] Error sending server offline webhook for {ServerName}", serverId, LogSanitizer.Sanitize(serverName)); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending server offline webhook for {ServerName}", serverId, serverName?.Replace("\r", "").Replace("\n", "")); } } @@ -529,7 +528,7 @@ public async Task SendServerOnlineAsync(int serverId, string serverName, int dow // Text message format - use custom text content if provided, otherwise default var textMessage = string.IsNullOrWhiteSpace(settings.ServerOnlineTextContent) - ? $"✅ **{serverName}** is back online! (Downtime: {downtimeText})" + ? $"✅ **{serverName}** is back online! (Downtime: {downtimeText})" : ReplaceVariables(settings.ServerOnlineTextContent, variables); // Default embed format @@ -539,13 +538,13 @@ public async Task SendServerOnlineAsync(int serverId, string serverName, int dow { new { - title = "✅ Server Online", + title = "✅ Server Online", description = $"**{serverName}** is back online!", color = 3066993, // Green fields = new[] { new { name = "Server", value = serverName, inline = true }, - new { name = "Status", value = "✅ Online", inline = true }, + new { name = "Status", value = "✅ Online", inline = true }, new { name = "Downtime", value = downtimeText, inline = false }, new { name = "Back Online At", value = $"", inline = false } }, @@ -568,7 +567,7 @@ public async Task SendServerOnlineAsync(int serverId, string serverName, int dow } catch (Exception ex) { - _logger.LogError(ex, "[SERVER {ServerId}] Error sending server online webhook for {ServerName}", serverId, LogSanitizer.Sanitize(serverName)); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending server online webhook for {ServerName}", serverId, serverName?.Replace("\r", "").Replace("\n", "")); } } @@ -630,7 +629,7 @@ public async Task SendServerProtectionAsync(int serverId, string playerName, str } catch (Exception ex) { - _logger.LogError(ex, "[SERVER {ServerId}] Error sending server protection webhook for {PlayerName}", serverId, LogSanitizer.Sanitize(playerName)); + _logger.LogError(ex, "[SERVER {ServerId}] Error sending server protection webhook for {PlayerName}", serverId, playerName?.Replace("\r", "").Replace("\n", "")); } } @@ -857,13 +856,13 @@ public async Task SendTestWebhookAsync(int serverId, string webhookUrl, WebhookE { new { - title = "🔴 Server Offline (TEST)", + title = "🔴 Server Offline (TEST)", description = $"**{serverName}** is no longer reachable", color = 15158332, fields = new[] { new { name = "Server", value = serverName, inline = true }, - new { name = "Status", value = "❌ Offline", inline = true }, + new { name = "Status", value = "❌ Offline", inline = true }, new { name = "Time", value = $"", inline = false } }, footer = new { text = "Server Status Monitor" }, @@ -871,7 +870,7 @@ public async Task SendTestWebhookAsync(int serverId, string webhookUrl, WebhookE } } }, - $"🔴 **{serverName}** is offline!" + $"🔴 **{serverName}** is offline!" ), WebhookEventType.ServerOnline => ( @@ -890,13 +889,13 @@ public async Task SendTestWebhookAsync(int serverId, string webhookUrl, WebhookE { new { - title = "✅ Server Online (TEST)", + title = "✅ Server Online (TEST)", description = $"**{serverName}** is back online!", color = 3066993, fields = new[] { new { name = "Server", value = serverName, inline = true }, - new { name = "Status", value = "✅ Online", inline = true }, + new { name = "Status", value = "✅ Online", inline = true }, new { name = "Downtime", value = "15 minutes", inline = false }, new { name = "Back Online At", value = $"", inline = false } }, @@ -905,7 +904,7 @@ public async Task SendTestWebhookAsync(int serverId, string webhookUrl, WebhookE } } }, - $"✅ **{serverName}** is back online! (Downtime: 15 minutes)" + $"✅ **{serverName}** is back online! (Downtime: 15 minutes)" ), WebhookEventType.ServerProtection => ( @@ -1007,12 +1006,12 @@ private async Task SendWebhookAsync(string webhookUrl, object payload, string ev else { var responseBody = await response.Content.ReadAsStringAsync(); - _logger.LogWarning("Failed to send {EventName} webhook. Status: {StatusCode}, Response: {ResponseBody}", eventName, response.StatusCode, LogSanitizer.Sanitize(responseBody)); + _logger.LogWarning("Failed to send {EventName} webhook. Status: {StatusCode}, Response: {ResponseBody}", eventName, response.StatusCode, responseBody?.Replace("\r", "").Replace("\n", "")); } } catch (Exception ex) { - _logger.LogError(ex, "Error sending {EventName} webhook to {WebhookUrl}", eventName, LogSanitizer.Sanitize(webhookUrl)); + _logger.LogError(ex, "Error sending {EventName} webhook to {WebhookUrl}", eventName, webhookUrl?.Replace("\r", "").Replace("\n", "")); } } @@ -1070,7 +1069,7 @@ private object ParseCustomContent(string customContent, Dictionary SendPasswordRecoveryEmailAsync(string toEmail, string co var host = _configuration["Smtp:Host"]; if (string.IsNullOrWhiteSpace(host)) { - _logger.LogWarning("SMTP is not configured (Smtp:Host is empty) - password recovery email was not sent to {MaskedEmail}", LogSanitizer.Sanitize(MaskEmail(toEmail))); + _logger.LogWarning("SMTP is not configured (Smtp:Host is empty) - password recovery email was not sent to {MaskedEmail}", MaskEmail(toEmail)?.Replace("\r", "").Replace("\n", "")); return false; } @@ -54,7 +53,7 @@ public async Task SendPasswordRecoveryEmailAsync(string toEmail, string co } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to send password recovery email to {MaskedEmail}", LogSanitizer.Sanitize(MaskEmail(toEmail))); + _logger.LogWarning(ex, "Failed to send password recovery email to {MaskedEmail}", MaskEmail(toEmail)?.Replace("\r", "").Replace("\n", "")); return false; } } diff --git a/RustRconServerManager.Backend/Services/MapStorageService.cs b/RustRconServerManager.Backend/Services/MapStorageService.cs index 15ebf54..05da9d7 100644 --- a/RustRconServerManager.Backend/Services/MapStorageService.cs +++ b/RustRconServerManager.Backend/Services/MapStorageService.cs @@ -222,7 +222,7 @@ public async Task SaveServerImageToDisk(string instanceHash, int serverId, strin await File.WriteAllBytesAsync(filePath, imageData); _logger.LogInformation("MapStorageService: Saved server image {Filename} for server {ServerId} to disk ({Size} bytes)", - LogSanitizer.Sanitize(filename), serverId, imageData.Length); + filename.Replace("\r", "").Replace("\n", ""), serverId, imageData.Length); } /// @@ -235,7 +235,7 @@ public void DeleteServerImageFromDisk(string instanceHash, int serverId, string { File.Delete(filePath); _logger.LogInformation("MapStorageService: Deleted server image {Filename} for server {ServerId}", - LogSanitizer.Sanitize(filename), serverId); + filename.Replace("\r", "").Replace("\n", ""), serverId); } } } diff --git a/RustRconServerManager.Backend/Services/PluginVersionCheckService.cs b/RustRconServerManager.Backend/Services/PluginVersionCheckService.cs index 9696c6a..b3ee64f 100644 --- a/RustRconServerManager.Backend/Services/PluginVersionCheckService.cs +++ b/RustRconServerManager.Backend/Services/PluginVersionCheckService.cs @@ -1,8 +1,7 @@ -using System.Text.Json; +using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using RustRconServerManager.Backend.Database; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.Models; using RustRconServerManager.Shared.PluginVersionCheck; @@ -10,7 +9,7 @@ namespace RustRconServerManager.Backend.Services; /// /// Service for checking plugin versions against Codefling and Umod. -/// Uses a local DB cache (PluginVersionCache) — entries expire after 30 minutes. +/// Uses a local DB cache (PluginVersionCache) — entries expire after 30 minutes. /// public class PluginVersionCheckService { @@ -46,23 +45,23 @@ public PluginVersionCheckService( if (pluginSource == null) { - _logger.LogInformation("[NO SOURCE] Plugin {PluginName} has no source configured for server {ServerId} - skipping version check", LogSanitizer.Sanitize(pluginName), serverId); + _logger.LogInformation("[NO SOURCE] Plugin {PluginName} has no source configured for server {ServerId} - skipping version check", pluginName?.Replace("\r", "").Replace("\n", ""), serverId); return null; } if (pluginSource.Source == PluginSource.Custom) { - _logger.LogInformation("[CUSTOM PLUGIN] Plugin {PluginName} is marked as custom for server {ServerId} - skipping version check", LogSanitizer.Sanitize(pluginName), serverId); + _logger.LogInformation("[CUSTOM PLUGIN] Plugin {PluginName} is marked as custom for server {ServerId} - skipping version check", pluginName?.Replace("\r", "").Replace("\n", ""), serverId); return null; } - _logger.LogInformation("[SOURCE CHECK] Plugin {PluginName} configured with source {Source} for server {ServerId}", LogSanitizer.Sanitize(pluginName), pluginSource.Source, serverId); + _logger.LogInformation("[SOURCE CHECK] Plugin {PluginName} configured with source {Source} for server {ServerId}", pluginName?.Replace("\r", "").Replace("\n", ""), pluginSource.Source, serverId); // Step 1: local cache var cached = await GetFromCacheAsync(pluginName); if (cached != null) { - _logger.LogInformation("[CACHE HIT] Plugin {PluginName} found in local cache", LogSanitizer.Sanitize(pluginName)); + _logger.LogInformation("[CACHE HIT] Plugin {PluginName} found in local cache", pluginName?.Replace("\r", "").Replace("\n", "")); return new PluginVersionCheckResult { PluginName = pluginName, @@ -74,7 +73,7 @@ public PluginVersionCheckService( }; } - _logger.LogInformation("[CACHE MISS] Plugin {PluginName} not in local cache, checking {Source} API...", LogSanitizer.Sanitize(pluginName), pluginSource.Source); + _logger.LogInformation("[CACHE MISS] Plugin {PluginName} not in local cache, checking {Source} API...", pluginName?.Replace("\r", "").Replace("\n", ""), pluginSource.Source); // Step 2: configured source PluginVersionCheckResult? result = null; @@ -107,7 +106,7 @@ public PluginVersionCheckService( return result; } - _logger.LogWarning("[NOT FOUND] Plugin {PluginName} not found on {Source}", LogSanitizer.Sanitize(pluginName), pluginSource.Source); + _logger.LogWarning("[NOT FOUND] Plugin {PluginName} not found on {Source}", pluginName?.Replace("\r", "").Replace("\n", ""), pluginSource.Source); return new PluginVersionCheckResult { PluginName = pluginName, @@ -121,7 +120,7 @@ public PluginVersionCheckService( } catch (Exception ex) { - _logger.LogError(ex, "Error checking version for plugin {PluginName}", LogSanitizer.Sanitize(pluginName)); + _logger.LogError(ex, "Error checking version for plugin {PluginName}", pluginName?.Replace("\r", "").Replace("\n", "")); return new PluginVersionCheckResult { PluginName = pluginName, @@ -154,7 +153,7 @@ public PluginVersionCheckService( .Where(sps => sps.RustServerId == serverId && pluginNames.Contains(sps.PluginName)) .ToDictionaryAsync(sps => sps.PluginName, sps => sps.Source); - // Plugins without a source / Custom plugins → null result + // Plugins without a source / Custom plugins → null result foreach (var plugin in plugins) { if (!pluginSources.TryGetValue(plugin.PluginName, out var source) || source == PluginSource.Custom) @@ -235,7 +234,7 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa existing.ExpiresAt = expiresAt; existing.UmodRateLimitRemaining = umodRateLimitRemaining; existing.UmodRateLimitTotal = umodRateLimitTotal; - _logger.LogInformation("[CACHE UPDATE] {PluginName} (expires {ExpiresAt:O})", LogSanitizer.Sanitize(result.PluginName), expiresAt); + _logger.LogInformation("[CACHE UPDATE] {PluginName} (expires {ExpiresAt:O})", result.PluginName?.Replace("\r", "").Replace("\n", ""), expiresAt); } else { @@ -250,14 +249,14 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa UmodRateLimitRemaining = umodRateLimitRemaining, UmodRateLimitTotal = umodRateLimitTotal }); - _logger.LogInformation("[CACHE SAVE] {PluginName} (expires {ExpiresAt:O})", LogSanitizer.Sanitize(result.PluginName), expiresAt); + _logger.LogInformation("[CACHE SAVE] {PluginName} (expires {ExpiresAt:O})", result.PluginName?.Replace("\r", "").Replace("\n", ""), expiresAt); } await _dbContext.SaveChangesAsync(); } catch (Exception ex) { - _logger.LogError(ex, "Error saving plugin {PluginName} to cache", LogSanitizer.Sanitize(result.PluginName)); + _logger.LogError(ex, "Error saving plugin {PluginName} to cache", result.PluginName?.Replace("\r", "").Replace("\n", "")); } } @@ -266,14 +265,14 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa try { var url = $"https://www.codefling.com/db/?category=all&filename={Uri.EscapeDataString(fileName)}"; - _logger.LogInformation("[CODEFLING] Checking filename '{FileName}' -> URL: {Url}", LogSanitizer.Sanitize(fileName), LogSanitizer.Sanitize(url)); + _logger.LogInformation("[CODEFLING] Checking filename '{FileName}' -> URL: {Url}", fileName?.Replace("\r", "").Replace("\n", ""), url?.Replace("\r", "").Replace("\n", "")); var response = await _httpClient.GetAsync(url); - _logger.LogInformation("[CODEFLING] Response for '{FileName}': StatusCode={StatusCode}", LogSanitizer.Sanitize(fileName), response.StatusCode); + _logger.LogInformation("[CODEFLING] Response for '{FileName}': StatusCode={StatusCode}", fileName?.Replace("\r", "").Replace("\n", ""), response.StatusCode); if (!response.IsSuccessStatusCode) { - _logger.LogWarning("[CODEFLING] Failed to fetch '{FileName}': {StatusCode}", LogSanitizer.Sanitize(fileName), response.StatusCode); + _logger.LogWarning("[CODEFLING] Failed to fetch '{FileName}': {StatusCode}", fileName?.Replace("\r", "").Replace("\n", ""), response.StatusCode); return null; } @@ -282,15 +281,15 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa var plugin = plugins?.FirstOrDefault(); if (plugin != null) - _logger.LogInformation("[CODEFLING] Found plugin '{Title}' v{Version}", LogSanitizer.Sanitize(plugin.Title), LogSanitizer.Sanitize(plugin.Version)); + _logger.LogInformation("[CODEFLING] Found plugin '{Title}' v{Version}", plugin.Title?.Replace("\r", "").Replace("\n", ""), plugin.Version?.Replace("\r", "").Replace("\n", "")); else - _logger.LogInformation("[CODEFLING] No plugin found for '{FileName}'", LogSanitizer.Sanitize(fileName)); + _logger.LogInformation("[CODEFLING] No plugin found for '{FileName}'", fileName?.Replace("\r", "").Replace("\n", "")); return plugin; } catch (Exception ex) { - _logger.LogWarning(ex, "[CODEFLING] Error checking {FileName}", LogSanitizer.Sanitize(fileName)); + _logger.LogWarning(ex, "[CODEFLING] Error checking {FileName}", fileName?.Replace("\r", "").Replace("\n", "")); return null; } } @@ -325,7 +324,7 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa { _logger.LogInformation("[UMOD RATE LIMIT] {RateLimitRemaining}/{RateLimitTotal} requests remaining", rateLimitRemaining, rateLimitTotal); if (rateLimitRemaining.Value < 5) - _logger.LogWarning("⚠️ [UMOD RATE LIMIT] Only {RateLimitRemaining} requests remaining!", rateLimitRemaining); + _logger.LogWarning("⚠️ [UMOD RATE LIMIT] Only {RateLimitRemaining} requests remaining!", rateLimitRemaining); } var content = await response.Content.ReadAsStringAsync(); @@ -344,7 +343,7 @@ private async Task SaveToCacheAsync(PluginVersionCheckResult result, int? umodRa } catch (Exception ex) { - _logger.LogWarning(ex, "Error checking Umod for {PluginName}", LogSanitizer.Sanitize(pluginName)); + _logger.LogWarning(ex, "Error checking Umod for {PluginName}", pluginName?.Replace("\r", "").Replace("\n", "")); return null; } } diff --git a/RustRconServerManager.Backend/Services/RconBackgroundService.Connections.cs b/RustRconServerManager.Backend/Services/RconBackgroundService.Connections.cs index 3dc13fe..c824c4c 100644 --- a/RustRconServerManager.Backend/Services/RconBackgroundService.Connections.cs +++ b/RustRconServerManager.Backend/Services/RconBackgroundService.Connections.cs @@ -1,7 +1,6 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.SignalR; using RustRconServerManager.Backend.Database; -using RustRconServerManager.Backend.Helpers; using RustRconServerManager.Backend.SignalRHubs; using RustRconServerManager.Backend.Models; using Xenne.RCON; @@ -33,13 +32,13 @@ private async Task StartConnectionWithServer(RconServer server) client.OnMessageReceived += async (sender, args) => { - _logger.LogInformation("[Server {ServerId}] Message: {Message}", args.ServerId, LogSanitizer.Sanitize(args.Message)); + _logger.LogInformation("[Server {ServerId}] Message: {Message}", args.ServerId, args.Message?.Replace("\r", "").Replace("\n", "")); await ServerMessageReceived(args.ServerId, args.Message); }; client.OnCommandAnswerReceived += async (sender, args) => { - _logger.LogDebug("[Server {ServerId}] Answer: {Message}", args.ServerId, LogSanitizer.Sanitize(args.Message)); + _logger.LogDebug("[Server {ServerId}] Answer: {Message}", args.ServerId, args.Message?.Replace("\r", "").Replace("\n", "")); await ServerCommandAnswerReceived(args.ServerId, args.Message, args.Command, args.Purpose); }; @@ -51,19 +50,19 @@ private async Task StartConnectionWithServer(RconServer server) client.OnChatMessageReceived += async (sender, args) => { - _logger.LogWarning("Global chat received: {ChatMessage}", LogSanitizer.Sanitize(args.ChatMessage)); + _logger.LogWarning("Global chat received: {ChatMessage}", args.ChatMessage?.Replace("\r", "").Replace("\n", "")); await OnChatReceived(args.ServerId, args.ChatMessage, args.PlayerId, args.PlayerName, args.Channel.ToString()); }; client.OnPlayerKill += async (sender, args) => { - _logger.LogWarning("Player killed: {KillerName} killed {VictimName}", LogSanitizer.Sanitize(args.KillerName), LogSanitizer.Sanitize(args.VictimName)); + _logger.LogWarning("Player killed: {KillerName} killed {VictimName}", args.KillerName?.Replace("\r", "").Replace("\n", ""), args.VictimName?.Replace("\r", "").Replace("\n", "")); await PlayerKilled(args.ServerId, args.KillerName, args.KillerId, args.VictimName, args.VictimId, args.Position); }; client.OnPlayerConnected += async (sender, args) => { - _logger.LogWarning("Player connected: {PlayerName}", LogSanitizer.Sanitize(args.PlayerName)); + _logger.LogWarning("Player connected: {PlayerName}", args.PlayerName?.Replace("\r", "").Replace("\n", "")); // Note: args.PlayerId contains the player NAME, args.PlayerName contains the SteamId await OnPlayerConnectedAsync(args.ServerId, args.PlayerName, args.PlayerId, args.PlayerEndpoint); }; @@ -71,14 +70,14 @@ private async Task StartConnectionWithServer(RconServer server) client.OnPlayerDisconnected += async (sender, args) => { _logger.LogInformation("Player disconnected: {PlayerName} ({PlayerId}) - Reason: {Reason}", - LogSanitizer.Sanitize(args.PlayerName), args.PlayerId, LogSanitizer.Sanitize(args.Reason)); + args.PlayerName?.Replace("\r", "").Replace("\n", ""), args.PlayerId, args.Reason?.Replace("\r", "").Replace("\n", "")); await OnPlayerDisconnectedAsync(args.ServerId, args.PlayerId, args.PlayerName, args.Reason); }; client.OnPlayerReported += async (sender, args) => { _logger.LogInformation("Player reported: {Reporter} reported {Reported} for {Type}", - LogSanitizer.Sanitize(args.ReporterName), LogSanitizer.Sanitize(args.ReportedName), LogSanitizer.Sanitize(args.Type)); + args.ReporterName?.Replace("\r", "").Replace("\n", ""), args.ReportedName?.Replace("\r", "").Replace("\n", ""), args.Type?.Replace("\r", "").Replace("\n", "")); await OnPlayerReportedAsync(args.ServerId, args.ReporterName, args.ReporterId, args.ReportedName, args.ReportedId, args.Subject, args.Message, args.Type); }; @@ -124,7 +123,7 @@ private async Task ServerMessageReceived(int serverId, string message) using var scope = _scopeFactory.CreateScope(); AppDbContext dbContext = scope.ServiceProvider.GetRequiredService(); - _logger.LogInformation("[RconBackgroundService] Message from Server {ServerId}: {Message}", serverId, LogSanitizer.Sanitize(message)); + _logger.LogInformation("[RconBackgroundService] Message from Server {ServerId}: {Message}", serverId, message?.Replace("\r", "").Replace("\n", "")); RconLogEntry logEntry = new RconLogEntry(); logEntry.CreatedAt = DateTime.UtcNow; From ac64bdd9a6b61b4a2df51eb2779f530ce028e41a Mon Sep 17 00:00:00 2001 From: Xenne <144433308+Xenne93@users.noreply.github.com> Date: Sun, 16 Aug 2026 13:31:26 +0200 Subject: [PATCH 4/4] Fix remaining 4 alerts: sanitize steamId logging, drop email from EmailService diagnostics - PanelSettingsController: sanitize steamId in VAC-ban override log calls (cs/log-forging) - EmailService: stop logging even a masked form of the recipient address, since CodeQL treats any value derived from the tainted parameter as still-sensitive regardless of transformation (cs/exposure-of-sensitive-information); removed the now-unused MaskEmail helper --- .../Controllers/PanelSettingsController.cs | 4 ++-- .../Services/EmailService.cs | 18 ++---------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs b/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs index 14bc14c..5688bb0 100644 --- a/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs +++ b/RustRconServerManager.Backend/Controllers/PanelSettingsController.cs @@ -512,7 +512,7 @@ public async Task> UpsertVacBanOverride await _dbContext.SaveChangesAsync(); _logger.LogInformation("[PanelSettingsController] VAC-ban override upserted for SteamID {SteamId} by {UserId}", - steamId, currentUser.Id); + steamId.Replace("\r", "").Replace("\n", ""), currentUser.Id); return Ok(new DeveloperVacBanOverrideDto { @@ -551,7 +551,7 @@ public async Task DeleteVacBanOverride(string steamId) return NotFound("Override not found"); _logger.LogInformation("[PanelSettingsController] VAC-ban override removed for SteamID {SteamId} by {UserId}", - steamId, currentUser.Id); + steamId.Replace("\r", "").Replace("\n", ""), currentUser.Id); return Ok(); } diff --git a/RustRconServerManager.Backend/Services/EmailService.cs b/RustRconServerManager.Backend/Services/EmailService.cs index 36f92b5..af731a2 100644 --- a/RustRconServerManager.Backend/Services/EmailService.cs +++ b/RustRconServerManager.Backend/Services/EmailService.cs @@ -19,7 +19,7 @@ public async Task SendPasswordRecoveryEmailAsync(string toEmail, string co var host = _configuration["Smtp:Host"]; if (string.IsNullOrWhiteSpace(host)) { - _logger.LogWarning("SMTP is not configured (Smtp:Host is empty) - password recovery email was not sent to {MaskedEmail}", MaskEmail(toEmail)?.Replace("\r", "").Replace("\n", "")); + _logger.LogWarning("SMTP is not configured (Smtp:Host is empty) - password recovery email was not sent"); return false; } @@ -53,22 +53,8 @@ public async Task SendPasswordRecoveryEmailAsync(string toEmail, string co } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to send password recovery email to {MaskedEmail}", MaskEmail(toEmail)?.Replace("\r", "").Replace("\n", "")); + _logger.LogWarning(ex, "Failed to send password recovery email"); return false; } } - - // Logs a partially redacted form of the address (e.g. "j***@example.com") so failures - // remain diagnosable without writing the full recipient email (PII) to the log sink. - private static string MaskEmail(string email) - { - if (string.IsNullOrEmpty(email)) - return "(empty)"; - - var atIndex = email.IndexOf('@'); - if (atIndex <= 0) - return "***"; - - return $"{email[0]}***{email.Substring(atIndex)}"; - } }