dgtlmoon/changedetection.io · warning
abort(404)
Error message
abort(404)
What it means
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'.
Source
Thrown at changedetectionio/flask_app.py:819
def before_request_handle_cookie_x_settings():
# Set the auth cookie path if we're running as X-settings/X-Forwarded-Prefix
if os.getenv('USE_X_SETTINGS') and 'X-Forwarded-Prefix' in request.headers:
app.config['REMEMBER_COOKIE_PATH'] = request.headers['X-Forwarded-Prefix']
app.config['SESSION_COOKIE_PATH'] = request.headers['X-Forwarded-Prefix']
return None
@app.route("/static/flags/<path:flag_path>", methods=['GET'])
def static_flags(flag_path):
"""Handle flag icon files with subdirectories"""
from flask import make_response
import re
# flag_path comes in as "1x1/de.svg" or "4x3/de.svg"
if re.match(r'^(1x1|4x3)/[a-z0-9-]+\.svg$', flag_path.lower()):
# Reconstruct the path safely with additional validation
parts = flag_path.lower().split('/')
if len(parts) != 2:
abort(404)
subdir = parts[0]
svg_file = parts[1]
# Extra validation: ensure subdir is exactly 1x1 or 4x3
if subdir not in ['1x1', '4x3']:
abort(404)
# Extra validation: ensure svg_file only contains safe characters
if not re.match(r'^[a-z0-9-]+\.svg$', svg_file):
abort(404)
try:
response = make_response(send_from_directory(f"static/flags/{subdir}", svg_file))
response.headers['Content-type'] = 'image/svg+xml'
response.headers['Cache-Control'] = 'max-age=86400, public' # Cache for 24 hours
return response
except FileNotFoundError:View on GitHub (pinned to 5d9c7c6da7)
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
Example fix
# before
flag_url = f'/flags/{locale.replace("_","/")}.svg'
# after
code = locale.split('_')[-1].lower()
flag_url = f'/flags/4x3/{code}.svg' Defensive patterns
Strategy: validation
Validate before calling
import re
def valid_flag_path(p: str) -> bool:
return bool(re.fullmatch(r'(1x1|4x3)/[a-z0-9-]+\.svg', p.lower())) and len(p.lower().split('/')) == 2 Type guard
def is_valid_flag_path(flag_path: str) -> bool:
"""True when the path is exactly <size>/<code>.svg."""
return bool(re.fullmatch(r'(1x1|4x3)/[a-z0-9-]+\.svg', flag_path.lower())) Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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).
Related errors
- No Favicon available for {uuid}
- abort(400) # Bad Request if the filename doesn't match the
- abort(404)
- Processor '{processor_name}' does not provide difference dat
- Processor '{processor_name}' does not support xlsx export
AI-assisted analysis of dgtlmoon/changedetection.io@5d9c7c6da7 (2026-08-27).
Data as JSON: /api/errors/88da5daed5b220f6.
Report an issue: GitHub.