Fitler paths in reported log data
This PR introduces a filter that redacts paths in log messages that are sent outside the current machine. This reduces the possibility that sensitive information is accidentially reported to an external party. For self-hosted solutions, the option `--allow-paths-in-log-messages=true` will skip the redaction step and report the original path.
This commit is contained in:
@@ -606,6 +606,8 @@ namespace Duplicati.Library.Main
|
||||
new CommandLineArgument("internal-profiling", CommandLineArgument.ArgumentType.Boolean, Strings.Options.InternalProfilingShort, Strings.Options.InternalProfilingLong, "false"),
|
||||
new CommandLineArgument("ignore-update-if-version-exists", CommandLineArgument.ArgumentType.Boolean, Strings.Options.IgnoreUpdateIfVersionExistsShort, Strings.Options.IgnoreUpdateIfVersionExistsLong, "false"),
|
||||
|
||||
new CommandLineArgument("allow-paths-in-log-messages", CommandLineArgument.ArgumentType.Boolean, Strings.Options.AllowPathsInLogMessagesShort, Strings.Options.AllowPathsInLogMessagesLong, "false"),
|
||||
|
||||
.. GetOSConditionalCommands(),
|
||||
.. GetDebugConditionalCommands(),
|
||||
];
|
||||
|
||||
@@ -410,6 +410,8 @@ namespace Duplicati.Library.Main.Strings
|
||||
public static string EnableAdsBackupLong { get { return LC.L("Use this option to enable backup of NTFS alternate data streams (ADS). This is a Windows-only feature that allows backing up hidden data attached to files."); } }
|
||||
public static string DisableAdsRestoreShort { get { return LC.L("Disable restore of NTFS alternate data streams"); } }
|
||||
public static string DisableAdsRestoreLong { get { return LC.L("Use this option to skip restoring NTFS alternate data streams (ADS) during a restore operation. The main file content will still be restored."); } }
|
||||
public static string AllowPathsInLogMessagesShort { get { return LC.L("Allow paths in log messages"); } }
|
||||
public static string AllowPathsInLogMessagesLong { get { return LC.L("Use this option to allow paths to be included in log messages sent to remote servers. By default, paths are redacted to protect sensitive information."); } }
|
||||
}
|
||||
|
||||
internal static class Common
|
||||
|
||||
@@ -89,6 +89,11 @@ namespace Duplicati.Library.Modules.Builtin
|
||||
/// </summary>
|
||||
protected abstract string ExtraDataOptionName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The option used to disable path redaction in log messages, mirrored from Options.cs
|
||||
/// </summary>
|
||||
protected const string OPTION_ALLOW_PATHS_IN_LOG_MESSAGES = "allow-paths-in-log-messages";
|
||||
|
||||
/// <summary>
|
||||
/// The default subject or title line
|
||||
/// </summary>
|
||||
@@ -205,6 +210,11 @@ namespace Duplicati.Library.Modules.Builtin
|
||||
/// </summary>
|
||||
private IResultFormatSerializer m_resultFormatSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// True if paths are allowed in log messages
|
||||
/// </summary>
|
||||
private bool m_allowPathsInLogMessages;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the module
|
||||
/// </summary>
|
||||
@@ -232,6 +242,7 @@ namespace Duplicati.Library.Modules.Builtin
|
||||
|
||||
m_options = commandlineOptions.AsReadOnly();
|
||||
m_isConfigured = true;
|
||||
m_allowPathsInLogMessages = Utility.Utility.ParseBoolOption(m_options, OPTION_ALLOW_PATHS_IN_LOG_MESSAGES);
|
||||
m_options.TryGetValue(SubjectOptionName, out m_subject);
|
||||
m_options.TryGetValue(BodyOptionName, out m_body);
|
||||
m_options.TryGetValue(ExtraDataOptionName, out var extraData);
|
||||
@@ -547,6 +558,9 @@ namespace Duplicati.Library.Modules.Builtin
|
||||
logdata = logdata.Concat(new string[] { $"... and {m_logstorage.Count - m_maxmimumLogLines} more" });
|
||||
}
|
||||
|
||||
if (!m_allowPathsInLogMessages)
|
||||
logdata = logdata.Select(x => SensitiveDataFilter.RedactPaths(x));
|
||||
|
||||
return logdata;
|
||||
}
|
||||
}
|
||||
@@ -608,6 +622,12 @@ namespace Duplicati.Library.Modules.Builtin
|
||||
body = ReplaceTemplate(body, result, exception, false);
|
||||
subject = ReplaceTemplate(subject, result, exception, true);
|
||||
|
||||
if (!m_allowPathsInLogMessages)
|
||||
{
|
||||
body = SensitiveDataFilter.RedactPaths(body);
|
||||
subject = SensitiveDataFilter.RedactPaths(subject);
|
||||
}
|
||||
|
||||
SendMessage(subject, body);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Duplicati.Library.UsageReporter
|
||||
public static void Report(string key, string data = null, ReportType type = ReportType.Information)
|
||||
{
|
||||
if (_eventChannel != null && type >= MaxReportLevel)
|
||||
try { _eventChannel.WriteNoWait(new ReportItem(type, null, key, data)); }
|
||||
try { _eventChannel.WriteNoWait(new ReportItem(type, null, key, SensitiveDataFilter.RedactPaths(data))); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace Duplicati.Library.UsageReporter
|
||||
public static void Report(Exception ex, ReportType type = ReportType.Warning)
|
||||
{
|
||||
if (_eventChannel != null && type >= MaxReportLevel)
|
||||
try { _eventChannel.WriteNoWait(new ReportItem(type, null, "EXCEPTION", ex.ToString())); }
|
||||
try { _eventChannel.WriteNoWait(new ReportItem(type, null, "EXCEPTION", SensitiveDataFilter.RedactPaths(ex.ToString()))); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (C) 2026, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Duplicati.Library.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to filter sensitive data from strings
|
||||
/// </summary>
|
||||
public static class SensitiveDataFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Regex that detects file system paths in text
|
||||
/// </summary>
|
||||
private static readonly Regex PathDetectionRegex = new Regex(
|
||||
@"(?<![\w/\\])" +
|
||||
@"(?:" +
|
||||
@"(?:[a-zA-Z]:\\(?:[^\\\s\n\r]*\\?)*)" + // Windows drive paths
|
||||
@"|(?:\\\\[^\\\s\n\r]+(?:\\[^\\\s\n\r]*)*)" + // UNC paths
|
||||
@"|(?:/[a-zA-Z0-9_.][^/\s\n\r]*(?:/[a-zA-Z0-9_.][^/\s\n\r]*)*)" + // Unix absolute paths
|
||||
@"|(?:file:///[^\s\n\r]*)" + // File URIs
|
||||
@")",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||
|
||||
/// <summary>
|
||||
/// Characters that are commonly trailing delimiters and should not be part of a path
|
||||
/// </summary>
|
||||
private static readonly char[] PathTrimChars = new[] { '"', '\'', '(', ')', '[', ']', '{', '}', '<', '>', ',', ';', ':' };
|
||||
|
||||
/// <summary>
|
||||
/// Replaces detected file system paths with "-redacted-"
|
||||
/// </summary>
|
||||
/// <param name="input">The input string to filter</param>
|
||||
/// <returns>The filtered string with paths redacted</returns>
|
||||
public static string RedactPaths(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return input;
|
||||
|
||||
return PathDetectionRegex.Replace(input, match =>
|
||||
{
|
||||
var value = match.Value;
|
||||
|
||||
// Trim trailing punctuation that is likely not part of the path
|
||||
int end = value.Length;
|
||||
while (end > 0 && Array.IndexOf(PathTrimChars, value[end - 1]) >= 0)
|
||||
end--;
|
||||
|
||||
if (end < value.Length)
|
||||
return "-redacted-" + value.Substring(end);
|
||||
|
||||
return "-redacted-";
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright (C) 2026, The Duplicati Team
|
||||
// https://duplicati.com, hello@duplicati.com
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the "Software"),
|
||||
// to deal in the Software without restriction, including without limitation
|
||||
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
// and/or sell copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using NUnit.Framework;
|
||||
using Duplicati.Library.Utility;
|
||||
|
||||
namespace Duplicati.UnitTest
|
||||
{
|
||||
[TestFixture]
|
||||
public class SensitiveDataFilterTests
|
||||
{
|
||||
[Test]
|
||||
public void RedactPaths_UnixPaths()
|
||||
{
|
||||
var input = "Failed to access /home/user/secret.txt";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Failed to access -redacted-"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_WindowsPaths()
|
||||
{
|
||||
var input = "Failed to access C:\\Users\\user\\secret.txt";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Failed to access -redacted-"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_UNCPaths()
|
||||
{
|
||||
var input = "Failed to access \\\\server\\share\\folder\\file.txt";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Failed to access -redacted-"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_FileUris()
|
||||
{
|
||||
var input = "Found file:///home/user/secret.txt";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Found -redacted-"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_MultiplePaths()
|
||||
{
|
||||
var input = "Paths: /home/user/secret.txt and C:\\Users\\user\\other.txt";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Paths: -redacted- and -redacted-"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_PathsInQuotes()
|
||||
{
|
||||
var input = "Path \"/home/user/secret.txt\" not found";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Path \"-redacted-\" not found"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_PathsInParens()
|
||||
{
|
||||
var input = "File (/home/user/secret.txt) missing";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("File (-redacted-) missing"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_StackTrace()
|
||||
{
|
||||
var input = "at Duplicati.Main.Run() in /Users/builder/project/Duplicati/Main.cs:line 42";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
// The path is redacted; trailing stack trace metadata may also be consumed
|
||||
Assert.That(result, Does.Contain("-redacted-"));
|
||||
Assert.That(result, Does.Not.Contain("/Users/builder/project/Duplicati/Main.cs"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_DoesNotRedactUrls()
|
||||
{
|
||||
var input = "Visit https://example.com/path/to/resource";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Visit https://example.com/path/to/resource"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_DoesNotRedactDates()
|
||||
{
|
||||
var input = "Date: 2026/01/01";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Date: 2026/01/01"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_DoesNotRedactDivision()
|
||||
{
|
||||
var input = "Result: 100 / 200";
|
||||
var result = SensitiveDataFilter.RedactPaths(input);
|
||||
Assert.That(result, Is.EqualTo("Result: 100 / 200"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RedactPaths_EmptyInput()
|
||||
{
|
||||
Assert.That(SensitiveDataFilter.RedactPaths(null), Is.Null);
|
||||
Assert.That(SensitiveDataFilter.RedactPaths(""), Is.EqualTo(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user