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 requirementsView on GitHub (pinned to 5d9c7c6da7)
Solutions
- Authenticate the request (session cookie or basic auth) before fetching screenshots
- Enable 'shared diff access' in settings if screenshots must be viewable without login
- 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
- Send credentials with every request to password-protected instances
- Enable shared_diff_access only when screenshots are meant to be public
- Never embed raw screenshot URLs in external systems without auth handling
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
- No screenshot available. Ensure the watch is configured to u
- UUID: {watch.get('uuid')} - Screenshot comparison failed: {e
- No Favicon available for {uuid}
- abort(400) # Bad Request if the filename doesn't match the
- abort(404)
AI-assisted analysis of dgtlmoon/changedetection.io@5d9c7c6da7 (2026-08-27).
Data as JSON: /api/errors/151425b4111395fa.
Report an issue: GitHub.