home-assistant/core · error · InvalidBackupFilename

Refusing to write outside {self._backup_dir}: {candidate}

Error message

Refusing to write outside {self._backup_dir}: {candidate}

What it means

Raised by LocalBackupAgent.get_new_backup_path when the candidate path built from suggested_filename(backup) would not land directly inside the configured backup directory (candidate.parent != self._backup_dir). Because suggested_filename does not strip path separators, a crafted backup name containing '/' or '..' could otherwise escape the backup directory; this guard blocks path traversal.

Source

Thrown at homeassistant/components/backup/backup.py:141

    @override
    def get_backup_path(self, backup_id: str) -> Path:
        """Return the local path to an existing backup.

        Raises BackupAgentError if the backup does not exist.
        """
        try:
            return self._backups[backup_id][1]
        except KeyError as err:
            raise BackupNotFound(f"Backup {backup_id} does not exist") from err

    @override
    def get_new_backup_path(self, backup: AgentBackup) -> Path:
        """Return the local path to a new backup."""
        candidate = self._backup_dir / suggested_filename(backup)
        # suggested_filename does not strip separators; refuse paths that would
        # land outside the backup directory.
        if candidate.parent != self._backup_dir:
            raise InvalidBackupFilename(
                f"Refusing to write outside {self._backup_dir}: {candidate}"
            )
        return candidate

    @override
    async def async_delete_backup(self, backup_id: str, **kwargs: Any) -> None:
        """Delete a backup file."""
        if not self._loaded_backups:
            await self._load_backups()

        backup_path = self.get_backup_path(backup_id)
        await self._hass.async_add_executor_job(backup_path.unlink, True)
        LOGGER.debug("Deleted backup located at %s", backup_path)
        self._backups.pop(backup_id)

View on GitHub (pinned to 58a3fdb3ea)

Solutions

  1. Sanitize the backup name before creating it: strip '/', '\\', '..' and other path characters from name and slugify it.
  2. If a filename with directories is genuinely needed, place files inside the backup dir manually rather than relying on the agent.
  3. Verify the backup's name field, not just its ID, when this error appears during receive/create flows.

Example fix

# before
await client.async_create_backup(name="../../etc/pwned")

# after
import re
safe_name = re.sub(r"[^\w\-. ]", "_", "../../etc/pwned")  # '.._.._etc_pwned'
await client.async_create_backup(name=safe_name)
Defensive patterns

Strategy: validation

Validate before calling

import re

def safe_backup_name(name: str) -> str:
    return re.sub(r"[^\w\-. ]", "_", name).strip(" .") or "backup"

candidate = backup_dir / safe_backup_name(name)
assert candidate.parent == backup_dir

Type guard

from pathlib import Path

def is_safe_backup_name(name: str, backup_dir: Path) -> bool:
    return (backup_dir / name).parent == backup_dir and name not in ("", ".", "..")

Try / catch

from homeassistant.components.backup.manager import InvalidBackupFilename

try:
    path = local_agent.get_new_backup_path(backup)
except InvalidBackupFilename as err:
    backup.name = safe_backup_name(backup.name)
    path = local_agent.get_new_backup_path(backup)

Prevention

When it happens

Trigger: Creating/uploading a backup whose name (fed through suggested_filename) contains a path separator — e.g., name="../../etc/evil" or "nested/dir/backup" — when the local agent computes the write path via get_new_backup_path. Also triggered by a legitimately odd name such as a trailing '/' or one that resolves differently after Path normalization.

Common situations: API clients or scripts creating backups with arbitrary, unsanitized names; malicious upload attempts against the backup upload endpoint; backups created on another system whose names contain slashes.

Related errors


AI-assisted analysis of home-assistant/core@58a3fdb3ea (2026-08-14). Data as JSON: /api/errors/8a67a2e0ccff8483. Report an issue: GitHub.