diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs
index 6a9a018d0..81319af3c 100644
--- a/Duplicati/Library/Main/Options.cs
+++ b/Duplicati/Library/Main/Options.cs
@@ -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(),
];
diff --git a/Duplicati/Library/Main/Strings.cs b/Duplicati/Library/Main/Strings.cs
index 7cde4102f..1992ab36f 100644
--- a/Duplicati/Library/Main/Strings.cs
+++ b/Duplicati/Library/Main/Strings.cs
@@ -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
diff --git a/Duplicati/Library/Modules/Builtin/ReportHelper.cs b/Duplicati/Library/Modules/Builtin/ReportHelper.cs
index 218d9b913..d2a5fde35 100644
--- a/Duplicati/Library/Modules/Builtin/ReportHelper.cs
+++ b/Duplicati/Library/Modules/Builtin/ReportHelper.cs
@@ -89,6 +89,11 @@ namespace Duplicati.Library.Modules.Builtin
///
protected abstract string ExtraDataOptionName { get; }
+ ///
+ /// The option used to disable path redaction in log messages, mirrored from Options.cs
+ ///
+ protected const string OPTION_ALLOW_PATHS_IN_LOG_MESSAGES = "allow-paths-in-log-messages";
+
///
/// The default subject or title line
///
@@ -205,6 +210,11 @@ namespace Duplicati.Library.Modules.Builtin
///
private IResultFormatSerializer m_resultFormatSerializer;
+ ///
+ /// True if paths are allowed in log messages
+ ///
+ private bool m_allowPathsInLogMessages;
+
///
/// Configures the module
///
@@ -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)
diff --git a/Duplicati/Library/UsageReporter/Reporter.cs b/Duplicati/Library/UsageReporter/Reporter.cs
index c8da26072..f919f60f6 100644
--- a/Duplicati/Library/UsageReporter/Reporter.cs
+++ b/Duplicati/Library/UsageReporter/Reporter.cs
@@ -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 { }
}
diff --git a/Duplicati/Library/Utility/SensitiveDataFilter.cs b/Duplicati/Library/Utility/SensitiveDataFilter.cs
new file mode 100644
index 000000000..0112df173
--- /dev/null
+++ b/Duplicati/Library/Utility/SensitiveDataFilter.cs
@@ -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
+{
+ ///
+ /// Helper class to filter sensitive data from strings
+ ///
+ public static class SensitiveDataFilter
+ {
+ ///
+ /// Regex that detects file system paths in text
+ ///
+ private static readonly Regex PathDetectionRegex = new Regex(
+ @"(?
+ /// Characters that are commonly trailing delimiters and should not be part of a path
+ ///
+ private static readonly char[] PathTrimChars = new[] { '"', '\'', '(', ')', '[', ']', '{', '}', '<', '>', ',', ';', ':' };
+
+ ///
+ /// Replaces detected file system paths with "-redacted-"
+ ///
+ /// The input string to filter
+ /// The filtered string with paths redacted
+ 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-";
+ });
+ }
+ }
+}
diff --git a/Duplicati/UnitTest/SensitiveDataFilterTests.cs b/Duplicati/UnitTest/SensitiveDataFilterTests.cs
new file mode 100644
index 000000000..de426197e
--- /dev/null
+++ b/Duplicati/UnitTest/SensitiveDataFilterTests.cs
@@ -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(""));
+ }
+ }
+}