{"record":{"id":"151425b4111395fa","repo":"dgtlmoon/changedetection.io","slug":"abort-403","errorCode":null,"errorMessage":"abort(403)","messagePattern":"abort\\(403\\)","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"changedetectionio/flask_app.py","lineNumber":859,"sourceCode":"\n    @app.route(\"/static/<string:group>/<string:filename>\", methods=['GET'])\n    def static_content(group, filename):\n        from flask import make_response\n        import re\n\n        # Strict sanitization: only allow a-z, 0-9, and underscore (blocks .. and other traversal)\n        group = re.sub(r'[^a-z0-9_-]+', '', group.lower())\n        filename = filename\n\n        # Additional safety: reject if sanitization resulted in empty strings\n        if not group or not filename:\n            abort(404)\n\n        if group == 'screenshot':\n            # Could be sensitive, follow password requirements\n            if datastore.data['settings']['application']['password'] and not flask_login.current_user.is_authenticated:\n                if not datastore.data['settings']['application'].get('shared_diff_access'):\n                    abort(403)\n\n            screenshot_filename = \"last-screenshot.png\" if not request.args.get('error_screenshot') else \"last-error-screenshot.png\"\n\n            # These files should be in our subdirectory\n            try:\n                # set nocache, set content-type\n                response = make_response(send_from_directory(os.path.join(datastore_o.datastore_path, filename), screenshot_filename))\n                response.headers['Content-type'] = 'image/png'\n                response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'\n                response.headers['Pragma'] = 'no-cache'\n                response.headers['Expires'] = 0\n                return response\n\n            except FileNotFoundError:\n                abort(404)\n\n        if group == 'favicon':\n            # Could be sensitive, follow password requirements","sourceCodeStart":841,"sourceCodeEnd":877,"githubUrl":"https://github.com/dgtlmoon/changedetection.io/blob/5d9c7c6da76340597243e8163c4f2439237fa0e8/changedetectionio/flask_app.py#L841-L877","documentation":"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.","triggerScenarios":"GET /static/screenshot/<uuid>.png while logged out, with settings.application.password set and settings.application.shared_diff_access unset/false.","commonSituations":"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.","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"],"exampleFix":"# before\nresp = requests.get(f'{base}/static/screenshot/{uuid}.png')  # 403\n\n# after\nfrom requests.auth import HTTPBasicAuth\nresp = requests.get(f'{base}/static/screenshot/{uuid}.png',\n                   auth=HTTPBasicAuth('admin@例子.com', app_password))","handlingStrategy":"validation","validationCode":"import requests\ndef can_fetch_screenshot(base, auth=None) -> bool:\n    return requests.get(f'{base}/static/screenshot/test', auth=auth).status_code != 403\n# 403 on unauthenticated probe means password protection is active","typeGuard":null,"tryCatchPattern":"try:\n    r = requests.get(screenshot_url, auth=auth)\n    r.raise_for_status()\nexcept requests.HTTPError as e:\n    if e.response.status_code == 403:\n        raise PermissionError('authenticate or enable shared_diff_access') from e\n    raise","preventionTips":["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"],"tags":["flask","authentication","http-403","screenshot","access-control"],"backgroundTag":"http-403-forbidden","analyzedSha":"5d9c7c6da76340597243e8163c4f2439237fa0e8","analyzedAt":"2026-08-27T19:41:16.067Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}