diff --git a/SECURITY.md b/SECURITY.md index 86ed356..aa0128e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -23,7 +23,7 @@ This will prevent local backups, so we need to think of a better zero knowledge # NPF-SEC-00005: Viewer mode can bypass permissions Since viewer mode requires actual knowledge of repo URI and repo password, there's no need to manage local permissions. -Viewer mode permissions are set to "restore". +Viewer mode permissions are set to "restore-only". # NPF-SEC-00006: Never inject permissions if some are already present @@ -60,4 +60,8 @@ Using obfuscation() symmetric function in order to not store the bare AES key. # NPF-SEC-00012: Don't add PRIVATE directory to wheel / bdist builds -The PRIVATE directory might contain alternative AES keys and obfuscation functions which should never be bundled for a PyPI release. \ No newline at end of file +The PRIVATE directory might contain alternative AES keys and obfuscation functions which should never be bundled for a PyPI release. + +# NPF-SEC-00013: Don't leave encrypted envrionment variables for script usage + +Sensible environment variables aren't available for scripts / additional parameters and will be replaced by a given string from __env__.py \ No newline at end of file diff --git a/npbackup/__env__.py b/npbackup/__env__.py index 4d4fdbb..00c2fc5 100644 --- a/npbackup/__env__.py +++ b/npbackup/__env__.py @@ -43,4 +43,8 @@ def set_build_type(build_type: str) -> None: BUILD_TYPE = build_type +# Allowed server ids for upgrade ALLOWED_UPGRADE_SERVER_IDS = ("npbackup.upgrader", "npbackup.deployment_server") + +# Replacement string for sensible data +HIDDEN_BY_NPBACKUP = "_[o_O]_hidden_by_npbackup" diff --git a/npbackup/core/runner.py b/npbackup/core/runner.py index 651e230..f9331a8 100644 --- a/npbackup/core/runner.py +++ b/npbackup/core/runner.py @@ -7,7 +7,7 @@ __intname__ = "npbackup.gui.core.runner" __author__ = "Orsiris de Jong" __copyright__ = "Copyright (C) 2022-2025 NetInvent" __license__ = "GPL-3.0-only" -__build__ = "2025021201" +__build__ = "2025021202" from typing import Optional, Callable, Union, List @@ -857,7 +857,6 @@ class NPBackupRunner: except KeyError: encrypted_env_variables = [] - env_variables += encrypted_env_variables expanded_env_vars = {} if isinstance(env_variables, list): for env_variable in env_variables: @@ -874,8 +873,26 @@ class NPBackupRunner: ) logger.debug("Trace:", exc_info=True) + expanded_encrypted_env_vars = {} + if isinstance(encrypted_env_variables, list): + for encrypted_env_variable in encrypted_env_variables: + if isinstance(encrypted_env_variable, dict): + for k, v in encrypted_env_variable.items(): + try: + v = os.path.expanduser(v) + v = os.path.expandvars(v) + expanded_encrypted_env_vars[k.strip()] = v.strip() + except Exception as exc: + self.write_logs( + f"Cannot expand encrypted environment variable {k}: {exc}", + level="error", + ) + logger.debug("Trace:", exc_info=True) try: self.restic_runner.environment_variables = expanded_env_vars + self.restic_runner.encrypted_environment_variables = ( + expanded_encrypted_env_vars + ) except ValueError: self.write_logs( "Cannot initialize additional environment variables", level="error" diff --git a/npbackup/restic_wrapper/__init__.py b/npbackup/restic_wrapper/__init__.py index e150e6b..4b5ab77 100644 --- a/npbackup/restic_wrapper/__init__.py +++ b/npbackup/restic_wrapper/__init__.py @@ -7,8 +7,8 @@ __intname__ = "npbackup.restic_wrapper" __author__ = "Orsiris de Jong" __copyright__ = "Copyright (C) 2022-2025 NetInvent" __license__ = "GPL-3.0-only" -__build__ = "2025012201" -__version__ = "2.3.6" +__build__ = "2025021201" +__version__ = "2.4.0" from typing import Tuple, List, Optional, Callable, Union @@ -28,6 +28,7 @@ from npbackup.__env__ import ( CHECK_INTERVAL, HEARTBEAT_INTERVAL, BUILD_TYPE, + HIDDEN_BY_NPBACKUP ) from npbackup.path_helper import CURRENT_DIR from npbackup.restic_wrapper import schema @@ -96,6 +97,7 @@ class ResticRunner: self._ignore_cloud_files = True self._additional_parameters = None self._environment_variables = {} + self._encrypted_environment_variables = {} # Function which will make executor abort if result is True self._stop_on = None @@ -137,6 +139,16 @@ class ResticRunner: ) os.environ[env_variable] = value + for ( + encrypted_env_variable, + value, + ) in self.encrypted_environment_variables.items(): + self.write_logs( + f'Setting encrypted envrionment variable "{encrypted_env_variable}"', + level="debug", + ) + os.environ[encrypted_env_variable] = value + # Configure default cpu usage when not specifically set if not "GOMAXPROCS" in self.environment_variables: nb_cores = os.cpu_count() @@ -154,11 +166,12 @@ class ResticRunner: """ Unsets repository & password environment, we don't need to keep that data when not requested """ - os.environ["RESTIC_PASSWORD"] = "o_O" + os.environ["RESTIC_PASSWORD"] = HIDDEN_BY_NPBACKUP os.environ["RESTIC_REPOSITORY"] = self.repository_anonymous - for env_variable in self.environment_variables.keys(): - os.environ[env_variable] = "__ooOO(° °)OOoo__" + # NPF-SEC-00013 Don't leave encrypted environment variables for script usage + for encrypted_env_variable in self.encrypted_environment_variables.keys(): + os.environ[encrypted_env_variable] = HIDDEN_BY_NPBACKUP @property def stdout(self) -> Optional[Union[int, str, Callable, queue.Queue]]: @@ -268,7 +281,7 @@ class ResticRunner: @property def repository_anonymous(self): if self.repository: - return self.repository.split(":")[0] + ":_(o_O)_hidden_by_npbackup" + return self.repository.split(":")[0] + ":" + HIDDEN_BY_NPBACKUP return None def write_logs( @@ -519,9 +532,19 @@ class ResticRunner: @environment_variables.setter def environment_variables(self, value): if not isinstance(value, dict): - raise ValueError("Bogus environment variables set") + raise ValueError(f"Bogus environment variables set: {value}") self._environment_variables = value + @property + def encrypted_environment_variables(self): + return self._encrypted_environment_variables + + @encrypted_environment_variables.setter + def encrypted_environment_variables(self, value): + if not isinstance(value, dict): + raise ValueError(f"Bogus encrypted environment variables set: {value}") + self._encrypted_environment_variables = value + @property def binary(self): return self._binary