{"record":{"id":"7d986ea6130cc868","repo":"dgtlmoon/changedetection.io","slug":"abort-400-bad-request-if-the-filename-doesn-t","errorCode":null,"errorMessage":"abort(400)  # Bad Request if the filename doesn't match the pattern","messagePattern":"abort\\(400\\)  # Bad Request if the filename doesn't match the pattern","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"changedetectionio/blueprint/backups/__init__.py","lineNumber":159,"sourceCode":"\n        return backup_info\n\n    @backups_blueprint.route(\"/download/<string:filename>\", methods=['GET'])\n    @login_optionally_required\n    def download_backup(filename):\n        import re\n        filename = filename.strip()\n        backup_filename_regex = BACKUP_FILENAME_FORMAT.format(r\"\\d+\")\n\n        # Resolve 'latest' before any validation so checks run against the real filename.\n        if filename == 'latest':\n            backups = find_backups()\n            if not backups:\n                abort(404)\n            filename = backups[0]['filename']\n\n        if not re.match(r\"^\" + backup_filename_regex + \"$\", filename):\n            abort(400)  # Bad Request if the filename doesn't match the pattern\n\n        full_path = os.path.join(os.path.abspath(datastore.datastore_path), filename)\n        if not full_path.startswith(os.path.abspath(datastore.datastore_path) + os.sep):\n            abort(404)\n\n        logger.debug(f\"Backup download request for '{full_path}'\")\n        return send_from_directory(os.path.abspath(datastore.datastore_path), filename, as_attachment=True)\n\n    @backups_blueprint.route(\"/\", methods=['GET'])\n    @backups_blueprint.route(\"/create\", methods=['GET'])\n    @login_optionally_required\n    def create():\n        backups = find_backups()\n        output = render_template(\"backup_create.html\",\n                                 available_backups=backups,\n                                 backup_running=any(thread.is_alive() for thread in backup_threads)\n                                 )\n        return output","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/dgtlmoon/changedetection.io/blob/5d9c7c6da76340597243e8163c4f2439237fa0e8/changedetectionio/blueprint/backups/__init__.py#L141-L177","documentation":"abort(400) raised in the backups blueprint when the requested backup filename does not match the expected backup filename regex. It is a deliberate input-validation guard: only filenames produced by changedetection.io's own backup mechanism (matching backup_filename_regex) are accepted for download.","triggerScenarios":"GET /backup/download?filename=<name> where filename contains characters or a shape not matching backup_filename_regex (path traversal attempts, wrong extension, hand-crafted names, URL-encoded slashes or '..' segments).","commonSituations":"Scripts or users tampering with the filename query parameter; passing a full absolute path instead of a bare filename; version changes that altered the backup naming scheme so old filenames no longer match the regex.","solutions":["Use only filenames returned by the backup listing endpoint (find_backups())","Pass the bare filename, not a path (no directories, no leading '/')","Check backup_filename_regex in changedetectionio/blueprint/backups/__init__.py and conform your filename to it","If your own backup files use a different naming scheme, rename them to match the regex or omit the filename param to get the newest backup"],"exampleFix":"# before\nrequests.get(f'{base}/backup/download', params={'filename': '/opt/data/2024-01-01-backup.zip'})\n\n# after\nbackups = requests.get(f'{base}/backup/list').json()\nnewest = backups[0]['filename']\nrequests.get(f'{base}/backup/download', params={'filename': newest})","handlingStrategy":"validation","validationCode":"import re\nfrom changedetectionio.blueprint.backups import backup_filename_regex  # or copy the pattern\ndef valid_backup_filename(name: str) -> bool:\n    return bool(re.fullmatch(backup_filename_regex, name)) and '/' not in name and '..' not in name","typeGuard":null,"tryCatchPattern":"try:\n    r = requests.get(url, params={'filename': name})\n    r.raise_for_status()\nexcept requests.HTTPError as e:\n    if e.response.status_code == 400:\n        raise ValueError(f'bad backup filename: {name}') from e\n    raise","preventionTips":["Always source filenames from the backup list endpoint","Never hand-build backup filenames","Re-fetch the regex from the installed version after upgrades"],"tags":["flask","backup","http-400","input-validation","path-traversal"],"backgroundTag":"request-validation-failed","analyzedSha":"5d9c7c6da76340597243e8163c4f2439237fa0e8","analyzedAt":"2026-08-27T19:41:16.067Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}