{"record":{"id":"88da5daed5b220f6","repo":"dgtlmoon/changedetection.io","slug":"abort-404-88da5d","errorCode":null,"errorMessage":"abort(404)","messagePattern":"abort\\(404\\)","errorType":"http","errorClass":null,"httpStatus":404,"severity":"warning","filePath":"changedetectionio/flask_app.py","lineNumber":819,"sourceCode":"    def before_request_handle_cookie_x_settings():\n        # Set the auth cookie path if we're running as X-settings/X-Forwarded-Prefix\n        if os.getenv('USE_X_SETTINGS') and 'X-Forwarded-Prefix' in request.headers:\n            app.config['REMEMBER_COOKIE_PATH'] = request.headers['X-Forwarded-Prefix']\n            app.config['SESSION_COOKIE_PATH'] = request.headers['X-Forwarded-Prefix']\n        return None\n\n    @app.route(\"/static/flags/<path:flag_path>\", methods=['GET'])\n    def static_flags(flag_path):\n        \"\"\"Handle flag icon files with subdirectories\"\"\"\n        from flask import make_response\n        import re\n\n        # flag_path comes in as \"1x1/de.svg\" or \"4x3/de.svg\"\n        if re.match(r'^(1x1|4x3)/[a-z0-9-]+\\.svg$', flag_path.lower()):\n            # Reconstruct the path safely with additional validation\n            parts = flag_path.lower().split('/')\n            if len(parts) != 2:\n                abort(404)\n\n            subdir = parts[0]\n            svg_file = parts[1]\n\n            # Extra validation: ensure subdir is exactly 1x1 or 4x3\n            if subdir not in ['1x1', '4x3']:\n                abort(404)\n\n            # Extra validation: ensure svg_file only contains safe characters\n            if not re.match(r'^[a-z0-9-]+\\.svg$', svg_file):\n                abort(404)\n\n            try:\n                response = make_response(send_from_directory(f\"static/flags/{subdir}\", svg_file))\n                response.headers['Content-type'] = 'image/svg+xml'\n                response.headers['Cache-Control'] = 'max-age=86400, public'  # Cache for 24 hours\n                return response\n            except FileNotFoundError:","sourceCodeStart":801,"sourceCodeEnd":837,"githubUrl":"https://github.com/dgtlmoon/changedetection.io/blob/5d9c7c6da76340597243e8163c4f2439237fa0e8/changedetectionio/flask_app.py#L801-L837","documentation":"First of several defensive 404s in the /flags/<path:flag_path> route: after lowercasing, the path is split on '/'; if it does not yield exactly two parts (subdir/file), the request is rejected. Flag paths must look like '1x1/de.svg' or '4x3/de.svg'.","triggerScenarios":"flag_path with more than one slash (nested dirs), no slash at all, or trailing slash — e.g. '1x1/eu/de.svg', 'de.svg', '1x1/de.svg/' — all produce != 2 parts after split.","commonSituations":"Templates building flag URLs from locale codes that include region subtags (e.g. 'en-US' mapped to 'us/united-states.svg'); upstream URL changes; hand-crafted requests with traversal-shaped paths (which the outer regex already blocks).","solutions":["Request flags in exactly the form <size>/<code>.svg with size in {1x1,4x3}","Sanitize locale-derived flag codes to a single [a-z0-9-]+ token before building the URL","Cache the correct flag URL pattern once instead of constructing it ad hoc"],"exampleFix":"# before\nflag_url = f'/flags/{locale.replace(\"_\",\"/\")}.svg'\n\n# after\ncode = locale.split('_')[-1].lower()\nflag_url = f'/flags/4x3/{code}.svg'","handlingStrategy":"validation","validationCode":"import re\ndef valid_flag_path(p: str) -> bool:\n    return bool(re.fullmatch(r'(1x1|4x3)/[a-z0-9-]+\\.svg', p.lower())) and len(p.lower().split('/')) == 2","typeGuard":"def is_valid_flag_path(flag_path: str) -> bool:\n    \"\"\"True when the path is exactly <size>/<code>.svg.\"\"\"\n    return bool(re.fullmatch(r'(1x1|4x3)/[a-z0-9-]+\\.svg', flag_path.lower()))","tryCatchPattern":null,"preventionTips":["Build flag URLs from a single helper that enforces the format","Map locale codes to simple hyphenated tokens","Add template tests for flag URL generation"],"tags":["flask","static-assets","flags","http-404","input-validation"],"backgroundTag":"request-validation-failed","analyzedSha":"5d9c7c6da76340597243e8163c4f2439237fa0e8","analyzedAt":"2026-08-27T19:41:16.067Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}