{"record":{"id":"a054d6a9636cd988","repo":"dgtlmoon/changedetection.io","slug":"abort-404","errorCode":null,"errorMessage":"abort(404)","messagePattern":"abort\\(404\\)","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"changedetectionio/blueprint/backups/__init__.py","lineNumber":163,"sourceCode":"    @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\n\n    @backups_blueprint.route(\"/remove-backups\", methods=['POST'])\n    @login_optionally_required\n    def remove_backups():","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/dgtlmoon/changedetection.io/blob/5d9c7c6da76340597243e8163c4f2439237fa0e8/changedetectionio/blueprint/backups/__init__.py#L145-L181","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\nfilename = '../../etc/passwd'\nrequests.get(f'{base}/backup/download', params={'filename': filename})\n\n# after\nfrom pathlib import PurePosixPath\nassert '..' not in PurePosixPath(filename).parts and '/' not in filename\nrequests.get(f'{base}/backup/download', params={'filename': filename})","handlingStrategy":"validation","validationCode":"from pathlib import PurePosixPath\ndef safe_backup_name(name: str) -> bool:\n    p = PurePosixPath(name)\n    return p.name == name and '..' not in p.parts and not name.startswith('/')","typeGuard":null,"tryCatchPattern":"try:\n    r = requests.get(url, params={'filename': name})\n    if r.status_code == 404:\n        raise PermissionError('filename escaped datastore path')\n    r.raise_for_status()\nexcept requests.RequestException as e:\n    log.warning('backup download rejected: %s', e)","preventionTips":["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"],"tags":["flask","path-traversal","http-404","security","backup"],"backgroundTag":"path-traversal-blocked","analyzedSha":"5d9c7c6da76340597243e8163c4f2439237fa0e8","analyzedAt":"2026-08-27T19:41:16.067Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}