{"record":{"id":"a21fce4835fe9b26","repo":"odysseus-dev/odysseus","slug":"image-not-found","errorCode":null,"errorMessage":"Image not found","messagePattern":"Image not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"app.py","lineNumber":517,"sourceCode":"@app.get(\"/api/generated-image/{filename}\")\nasync def serve_generated_image(filename: str, request: Request):\n    \"\"\"Serve generated images from the data directory.\"\"\"\n    img_path = resolve_generated_image_path(filename)\n    # SECURITY: filename is the only key, so anyone who knows / guesses a\n    # 12-hex content hash could pull another user's image bytes. Require\n    # auth and verify ownership via the gallery row (when one exists).\n    try:\n        from src.auth_helpers import get_current_user\n        from core.database import SessionLocal as _SL, GalleryImage as _GI\n        _user = get_current_user(request)\n        if _user:\n            _db = _SL()\n            try:\n                _row = _db.query(_GI).filter(_GI.filename == filename).first()\n                # Generated-but-not-yet-imported images have no row → allow.\n                # Row exists with a different owner → 404 (don't confirm existence).\n                if _row is not None and _row.owner and _row.owner != _user:\n                    raise HTTPException(status_code=404, detail=\"Image not found\")\n            finally:\n                _db.close()\n    except HTTPException:\n        raise\n    except Exception as _e:\n        logger.warning(\"Image ownership verification failed for %r\", filename, exc_info=_e)\n    ext = filename.rsplit('.', 1)[-1].lower()\n    mime = {\n        \"png\": \"image/png\", \"jpg\": \"image/jpeg\", \"jpeg\": \"image/jpeg\",\n        \"webp\": \"image/webp\", \"gif\": \"image/gif\",\n        \"mp4\": \"video/mp4\", \"mov\": \"video/quicktime\", \"webm\": \"video/webm\",\n        \"mkv\": \"video/x-matroska\", \"m4v\": \"video/mp4\",\n    }.get(ext, \"application/octet-stream\")\n    # Generated-image filenames are content hashes → the bytes for a given\n    # filename never change. Cache them hard so the gallery doesn't\n    # re-download every full-size image each time it's opened. `immutable`\n    # tells the browser it never needs to revalidate within the max-age.\n    return FileResponse(","sourceCodeStart":499,"sourceCodeEnd":535,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/app.py#L499-L535","documentation":"Raised as HTTPException(404) during image ownership verification before serving a gallery file. The code looks up the GalleryImage row by filename; a row owned by a different user than the authenticated requester is deliberately returned as 404 so the endpoint does not leak which filenames exist.","triggerScenarios":"Requesting /image/{filename} (or equivalent) while authenticated as user A for an image whose gallery row's owner is user B. Also note the guard only raises when the row exists with a different non-null owner; no-row (generated but unimported) is allowed through.","commonSituations":"Shared or guessed filenames across multi-user deployments; a user re-logging as a different account while the browser still requests another user's image; ownership fields changed by an import/migration.","solutions":["Access the image while authenticated as its owner","If ownership looks wrong, correct the GalleryImage.owner row in the database","Do not rely on this path for secrecy of unimported files — filenames without rows are served; import or protect them separately"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"# Server-side pre-check before rendering links\nrow = db.query(GalleryImage).filter(GalleryImage.filename == fn).first()\nif row is not None and row.owner and row.owner != current_user:\n    continue  # skip linking images the user cannot fetch","typeGuard":null,"tryCatchPattern":"# Caller of the image endpoint\nresp = requests.get(url, auth=...)\nif resp.status_code == 404:\n    raise FileNotFoundError(filename)  # treat as inaccessible","preventionTips":["Only generate image URLs for rows the requester owns","Treat 404 as 'not yours or not exists' — do not retry with other users' credentials","Import generated images promptly so ownership is recorded"],"tags":["authorization","http","images","multi-user"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}