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

  1. Request flags in exactly the form <size>/<code>.svg with size in {1x1,4x3}
  2. Sanitize locale-derived flag codes to a single [a-z0-9-]+ token before building the URL
  3. 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

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


AI-assisted analysis of dgtlmoon/changedetection.io@5d9c7c6da7 (2026-08-27). Data as JSON: /api/errors/88da5daed5b220f6. Report an issue: GitHub.