Files
duplicati/Duplicati/Library/AutoUpdater/DataFolderLocator.cs
T
Kenneth Skovhede a7ef8e42f2 Lock permissions on datafolder
This PR is a security hardning change and will break some setups.

The hardning is done to ensure the datafolder is not compromised and particularly that it is not maliciously seeded.

The changes now **requires** that the datafolder is locked down with exact permissions, or Duplicati will refuse to use it. This is a change to prior functionality where the folder would simply be locked down if it was not already locked.

Previously, a file named `insecure-permissions.txt` could be placed in the folder to instruct Duplicati not to set permissions. With this change that file is no longer supported.

The only way to opt-out of the permission check is to supply `--allow-insecure-datafolder` or the environment variable `DUPLICATI__ALLOW_INSECURE_DATAFOLDER`. Once either of those are set, Duplicati will no longer check permissions and accept folders with other permissions.

The setting can be applied to `preload.json`, but the file needs to be in a trusted location, which is either the directory with the binaries or the file pointed to by `DUPLICATI_PRELOAD_SETTINGS`. The setting cannot be placed in a `preload.json` file that is in an insecure folder as the folder is not read if the permissions are not correct.

The previous paths `/usr/local/share/Duplicati/preload.json` and `C:\ProgramData\Duplicati\preload.json` are no longer supported as they cannot be guaranteed to be locked down.

A `preload.json` in the datafolder is still supported, provided the folder passes the permission check (or the `--allow-insecure-datafolder` is passed to the executable).

For most users, this should not cause any problems as Duplicati has been locking down the folders, so they should be correctly locked down already.

The configuretool has been updated with a `secure-datafolder` command that can be used to force the correct permissions on a folder, in case the folder did not have the correct permissions.

For some Docker setups it may not be possible to set the permissions and the check will fail. These setup will need to apply `DUPLICATI__ALLOW_INSECURE_DATAFOLDER=true` in the image to run without the protections.
2026-07-10 15:03:32 +02:00

158 lines
7.3 KiB
C#

// 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.Linq;
using System.Collections.Generic;
using Duplicati.Library.Common.IO;
#nullable enable
namespace Duplicati.Library.AutoUpdater;
/// <summary>
/// Helper class to locate the default storage folder for the application
/// </summary>
public static class DataFolderLocator
{
/// <summary>
/// The log tag for this class
/// </summary>
private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(DataFolderLocator));
/// <summary>
/// Finds a default storage folder, using the operating system specific locations.
/// The targetfilename is used to detect locations that are used in previous versions.
/// If the targetfilename is found in an old location, but not the current, the old location is used.
/// Note that the folder is not created, only the path is returned.
/// If the data folder is overriden, the overriden folder is used, and no search is performed.
/// </summary>
/// <param name="targetfilename">The filename to look for</param>
/// <param name="autoCreate">Whether to create the folder if it does not exist</param>
/// <returns>The default storage folder</returns>
public static string GetDefaultStorageFolder(string targetfilename, bool autoCreate, bool readOnly = false)
{
var folder = DataFolderManager.OVERRIDEN_DATAFOLDER
? DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ProbeOnly)
: GetDefaultStorageFolderInternal(targetfilename, AutoUpdateSettings.AppName);
// In read-only mode we neither create nor lock down the folder, but we still verify
// that an existing folder has the canonical restricted permissions, so a squatted or
// seeded folder is not silently trusted.
if (readOnly)
{
if (SystemIO.IO_OS.DirectoryExists(folder))
DataFolderManager.VerifyDataFolderSecurityReadOnly(folder);
return folder;
}
// Route through the shared helper so the CLI, Agent and ServerUtil get the same
// folder-squatting / seeded-content protection as the Server. A folder created here is
// locked down; a pre-existing folder is verified as-is (never modified) and rejected if
// it is not already in the canonical locked-down form.
if (SystemIO.IO_OS.DirectoryExists(folder) || autoCreate)
DataFolderManager.PrepareSecureDataFolder(folder, createIfMissing: autoCreate);
return folder;
}
/// <summary>
/// Finds a default storage folder, using the operating system specific locations.
/// The targetfilename is used to detect locations that are used in previous versions.
/// If the targetfilename is found in an old location, but not the current, the old location is used.
/// Note that the folder is not created, only the path is returned.
/// </summary>
/// <param name="targetfilename">The filename to look for</param>
/// <param name="appName">The name of the application</param>
/// <returns>The default storage folder</returns>
internal static string GetDefaultStorageFolderInternal(string targetfilename, string appName)
{
//Normal mode uses the systems "(Local) Application Data" folder
// %LOCALAPPDATA% on Windows, ~/.config on Linux
var folder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), appName);
if (OperatingSystem.IsWindows())
{
// Special handling for Windows:
// - Older versions use %APPDATA%
// - New versions use %LOCALAPPDATA%
// - And prevent using C:\Windows\
var newlocation = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), appName);
var folderOrder = new List<string>() {
folder,
newlocation
};
// If %LOCALAPPDATA% is inside the Windows folder, prefer the ProgramData folder instead
if (Common.IO.Util.IsPathUnderWindowsFolder(newlocation))
folderOrder.Add(System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), appName));
// Prefer the most recent location
var matches = folderOrder.AsEnumerable()
.Reverse()
.Where(x => System.IO.File.Exists(System.IO.Path.Combine(x, targetfilename)))
.ToList();
// Use the most recent location found with content
// If none are found, use the most recent location
folder = matches.FirstOrDefault() ?? folderOrder.Last();
}
if (OperatingSystem.IsMacOS())
{
// Special handling for MacOS:
// - Older versions use ~/.config/
// - but new versions use ~/Library/Application\ Support/
var configfolder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", appName);
var prevfile = System.IO.Path.Combine(configfolder, targetfilename);
var curfile = System.IO.Path.Combine(folder, targetfilename);
// If the old file exists, and not the new file, we use the old
// Otherwise we use the new location
if (!System.IO.File.Exists(curfile) && System.IO.File.Exists(prevfile))
folder = configfolder;
}
if (OperatingSystem.IsLinux() && (folder == $"/{appName}" || folder == appName))
{
// Special handling for Linux with no home folder:
// - Older versions use /
// - but new versions use /var/lib/
var libfolder = System.IO.Path.Combine("var", "lib", appName);
var curfile = System.IO.Path.Combine(libfolder, targetfilename);
var prevfile = System.IO.Path.Combine(folder, targetfilename);
// If the old file exists, and not the new file, we use the old
// Otherwise we use the new location
if (System.IO.File.Exists(curfile) || !System.IO.File.Exists(prevfile))
folder = libfolder;
}
return folder;
}
}