getgrav/grav · error · RuntimeException
Invalid backup location: {$backup_root}
Error message
Invalid backup location: {$backup_root} What it means
Before archiving, Backups::backup() canonicalizes both the backup root and GRAV_ROOT with realpath(). If either call returns false — realpath fails on nonexistent paths, broken symlink loops, or paths outside open_basedir restrictions — it throws RuntimeException 'Invalid backup location: ...'. This is the sanity stage between the existence check and the containment check.
Source
Thrown at system/src/Grav/Common/Backup/Backups.php:264
$max_execution_time = ini_set('max_execution_time', '600');
$backup_root = $backup->root;
if ($locator->isStream($backup_root)) {
$backup_root = $locator->findResource($backup_root);
} else {
$backup_root = rtrim(GRAV_ROOT . $backup_root, DS) ?: DS;
}
if (!$backup_root || !file_exists($backup_root)) {
throw new RuntimeException("Backup location: {$backup_root} does not exist...");
}
// Security: Resolve real path and ensure it's within GRAV_ROOT to prevent path traversal
$realBackupRoot = realpath($backup_root);
$realGravRoot = realpath(GRAV_ROOT);
if ($realBackupRoot === false || $realGravRoot === false) {
throw new RuntimeException("Invalid backup location: {$backup_root}");
}
// Positive containment (GHSA-fch7-cpv4-w7hg): the resolved backup root must
// BE GRAV_ROOT or a directory beneath it. The previous deny-list only rejected
// a fixed set of system paths, so a non-blocklisted external directory (e.g.
// /opt, /mnt, /srv) still fell through and had its contents archived. Comparing
// against GRAV_ROOT with a trailing separator also prevents a sibling directory
// (e.g. `/var/www/site-evil` next to `/var/www/site`) from matching by prefix.
$isWithinGravRoot = $realBackupRoot === $realGravRoot
|| strpos($realBackupRoot, $realGravRoot . DIRECTORY_SEPARATOR) === 0;
if (!$isWithinGravRoot) {
throw new RuntimeException("Backup location not allowed (outside site root): {$backup_root}");
}
$backup_root = $realBackupRoot;
$options = [
'exclude_files' => static::convertExclude($backup->exclude_files ?? ''),View on GitHub (pinned to 6040efed04)
Solutions
- Add the site root (and backup destination) to open_basedir, or disable the restriction if the environment allows
- Replace symlink chains in the backup root with the real directory path
- Run var_dump(realpath(GRAV_ROOT), realpath($root)) in a scratch script to identify which of the two fails, then fix that path
- Point the profile root at a plain directory inside the site instead of a symlinked one
Example fix
// before — profile root passes through a broken symlink $root = '/shared-site'; // /shared-site -> /mnt/old (missing target) // after $root = '/user'; // real directory inside GRAV_ROOT; realpath() succeeds
Defensive patterns
Strategy: validation
Validate before calling
$realRoot = realpath($resolvedRoot);
$realGrav = realpath(GRAV_ROOT);
if ($realRoot === false || $realGrav === false) {
// realpath failed: open_basedir, broken symlink chain, or vanished path
throw new RuntimeException("Cannot canonicalize {$resolvedRoot} — check open_basedir and symlinks");
} Prevention
- Keep open_basedir inclusive of GRAV_ROOT and all backup destinations
- Avoid symlink chains in backup roots; realpath() must resolve every link
- Smoke-test backup profiles after PHP hardening changes — realpath failures appear only at runtime
When it happens
Trigger: The backup root contains a symlink whose target is a broken symlink or a loop; PHP open_basedir excludes the resolved path so realpath() returns false; the path was deleted between the file_exists() check and the realpath() call (race); GRAV_ROOT itself is unreachable under the current open_basedir (common in hardened shared-hosting PHP-FPM pools).
Common situations: Shared hosting with restrictive open_basedir that does not include the site root; symlinked docroots (e.g. /www -> /var/www) where a link in the chain is broken after server reconfiguration; security hardening applied to php.ini after backups previously worked.
Related errors
- Backup location: {$backup_root} does not exist...
- Failed to save file {filepath}
- No backups defined...
- Backup location not allowed (outside site root): {$backup_ro
- Unable to create directory: %s
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/922697521e5e34c4.
Report an issue: GitHub.