dgtlmoon/changedetection.io · error

abort(403)

Error message

abort(403)

What it means

403 in the static content route for the 'screenshot' group: when an application password is configured and the current user is not authenticated, and shared_diff_access is not enabled, screenshot files are treated as sensitive and access is forbidden.

Source

Thrown at changedetectionio/flask_app.py:859

    @app.route("/static/<string:group>/<string:filename>", methods=['GET'])
    def static_content(group, filename):
        from flask import make_response
        import re

        # Strict sanitization: only allow a-z, 0-9, and underscore (blocks .. and other traversal)
        group = re.sub(r'[^a-z0-9_-]+', '', group.lower())
        filename = filename

        # Additional safety: reject if sanitization resulted in empty strings
        if not group or not filename:
            abort(404)

        if group == 'screenshot':
            # Could be sensitive, follow password requirements
            if datastore.data['settings']['application']['password'] and not flask_login.current_user.is_authenticated:
                if not datastore.data['settings']['application'].get('shared_diff_access'):
                    abort(403)

            screenshot_filename = "last-screenshot.png" if not request.args.get('error_screenshot') else "last-error-screenshot.png"

            # These files should be in our subdirectory
            try:
                # set nocache, set content-type
                response = make_response(send_from_directory(os.path.join(datastore_o.datastore_path, filename), screenshot_filename))
                response.headers['Content-type'] = 'image/png'
                response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
                response.headers['Pragma'] = 'no-cache'
                response.headers['Expires'] = 0
                return response

            except FileNotFoundError:
                abort(404)

        if group == 'favicon':
            # Could be sensitive, follow password requirements

View on GitHub (pinned to 5d9c7c6da7)

Solutions

  1. Authenticate the request (session cookie or basic auth) before fetching screenshots
  2. Enable 'shared diff access' in settings if screenshots must be viewable without login
  3. For API use, send credentials with the request instead of a bare URL

Example fix

# before
resp = requests.get(f'{base}/static/screenshot/{uuid}.png')  # 403

# after
from requests.auth import HTTPBasicAuth
resp = requests.get(f'{base}/static/screenshot/{uuid}.png',
                   auth=HTTPBasicAuth('admin@例子.com', app_password))
Defensive patterns

Strategy: validation

Validate before calling

import requests
def can_fetch_screenshot(base, auth=None) -> bool:
    return requests.get(f'{base}/static/screenshot/test', auth=auth).status_code != 403
# 403 on unauthenticated probe means password protection is active

Try / catch

try:
    r = requests.get(screenshot_url, auth=auth)
    r.raise_for_status()
except requests.HTTPError as e:
    if e.response.status_code == 403:
        raise PermissionError('authenticate or enable shared_diff_access') from e
    raise

Prevention

When it happens

Trigger: GET /static/screenshot/<uuid>.png while logged out, with settings.application.password set and settings.application.shared_diff_access unset/false.

Common situations: Embedding screenshot URLs in external tools/notifications while the instance is password-protected; API scripts that don't carry the session/basic-auth credentials; shared_diff access accidentally disabled when sharing diffs was intended.

Related errors


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