dgtlmoon/changedetection.io · error
abort(404)
Error message
abort(404)
What it means
abort(404) in the backup download endpoint guarding against path traversal: after joining the requested filename with the datastore path, the resulting full path must still start with the datastore directory plus a separator. If not, the resolved path escaped the datastore directory and the request is rejected as 404.
Source
Thrown at changedetectionio/blueprint/backups/__init__.py:163
@login_optionally_required
def download_backup(filename):
import re
filename = filename.strip()
backup_filename_regex = BACKUP_FILENAME_FORMAT.format(r"\d+")
# Resolve 'latest' before any validation so checks run against the real filename.
if filename == 'latest':
backups = find_backups()
if not backups:
abort(404)
filename = backups[0]['filename']
if not re.match(r"^" + backup_filename_regex + "$", filename):
abort(400) # Bad Request if the filename doesn't match the pattern
full_path = os.path.join(os.path.abspath(datastore.datastore_path), filename)
if not full_path.startswith(os.path.abspath(datastore.datastore_path) + os.sep):
abort(404)
logger.debug(f"Backup download request for '{full_path}'")
return send_from_directory(os.path.abspath(datastore.datastore_path), filename, as_attachment=True)
@backups_blueprint.route("/", methods=['GET'])
@backups_blueprint.route("/create", methods=['GET'])
@login_optionally_required
def create():
backups = find_backups()
output = render_template("backup_create.html",
available_backups=backups,
backup_running=any(thread.is_alive() for thread in backup_threads)
)
return output
@backups_blueprint.route("/remove-backups", methods=['POST'])
@login_optionally_required
def remove_backups():View on GitHub (pinned to 5d9c7c6da7)
Solutions
- Pass only a plain filename previously listed by the backup listing endpoint
- URL-decode and reject any filename containing '/', '\\' or '..' before sending the request
- Verify datastore.datastore_path is the same directory your backups live in
Example fix
# before
filename = '../../etc/passwd'
requests.get(f'{base}/backup/download', params={'filename': filename})
# after
from pathlib import PurePosixPath
assert '..' not in PurePosixPath(filename).parts and '/' not in filename
requests.get(f'{base}/backup/download', params={'filename': filename}) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import PurePosixPath
def safe_backup_name(name: str) -> bool:
p = PurePosixPath(name)
return p.name == name and '..' not in p.parts and not name.startswith('/') Try / catch
try:
r = requests.get(url, params={'filename': name})
if r.status_code == 404:
raise PermissionError('filename escaped datastore path')
r.raise_for_status()
except requests.RequestException as e:
log.warning('backup download rejected: %s', e) Prevention
- Treat any filename containing '/', '\\', or '..' as invalid client-side
- URL-encode filename params and reject decoded traversal
- Store only trusted filenames from the listing API
When it happens
Trigger: filename values like '../secret.zip', '..%2f..%2fetc%2cpasswd', absolute paths, or symlinks that resolve outside datastore.datastore_path produce a full_path that fails the startswith(datastore_path + os.sep) check.
Common situations: Path traversal attempts (often automated scanners); passing an absolute path as filename; running the app on a case-sensitive filesystem where the datastore path casing differs between config and request.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- abort(400) # Bad Request if the filename doesn't match the
- No Favicon available for {uuid}
- Processor '{processor_name}' does not provide difference dat
- Processor '{processor_name}' does not support xlsx export
- Asset '{asset_name}' not found
AI-assisted analysis of dgtlmoon/changedetection.io@5d9c7c6da7 (2026-08-27).
Data as JSON: /api/errors/a054d6a9636cd988.
Report an issue: GitHub.