odysseus-dev/odysseus · warning · HTTPException

Image not found

Error message

Image not found

What it means

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.

Source

Thrown at app.py:517

@app.get("/api/generated-image/{filename}")
async def serve_generated_image(filename: str, request: Request):
    """Serve generated images from the data directory."""
    img_path = resolve_generated_image_path(filename)
    # SECURITY: filename is the only key, so anyone who knows / guesses a
    # 12-hex content hash could pull another user's image bytes. Require
    # auth and verify ownership via the gallery row (when one exists).
    try:
        from src.auth_helpers import get_current_user
        from core.database import SessionLocal as _SL, GalleryImage as _GI
        _user = get_current_user(request)
        if _user:
            _db = _SL()
            try:
                _row = _db.query(_GI).filter(_GI.filename == filename).first()
                # Generated-but-not-yet-imported images have no row → allow.
                # Row exists with a different owner → 404 (don't confirm existence).
                if _row is not None and _row.owner and _row.owner != _user:
                    raise HTTPException(status_code=404, detail="Image not found")
            finally:
                _db.close()
    except HTTPException:
        raise
    except Exception as _e:
        logger.warning("Image ownership verification failed for %r", filename, exc_info=_e)
    ext = filename.rsplit('.', 1)[-1].lower()
    mime = {
        "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg",
        "webp": "image/webp", "gif": "image/gif",
        "mp4": "video/mp4", "mov": "video/quicktime", "webm": "video/webm",
        "mkv": "video/x-matroska", "m4v": "video/mp4",
    }.get(ext, "application/octet-stream")
    # Generated-image filenames are content hashes → the bytes for a given
    # filename never change. Cache them hard so the gallery doesn't
    # re-download every full-size image each time it's opened. `immutable`
    # tells the browser it never needs to revalidate within the max-age.
    return FileResponse(

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Access the image while authenticated as its owner
  2. If ownership looks wrong, correct the GalleryImage.owner row in the database
  3. Do not rely on this path for secrecy of unimported files — filenames without rows are served; import or protect them separately
Defensive patterns

Strategy: validation

Validate before calling

# Server-side pre-check before rendering links
row = db.query(GalleryImage).filter(GalleryImage.filename == fn).first()
if row is not None and row.owner and row.owner != current_user:
    continue  # skip linking images the user cannot fetch

Try / catch

# Caller of the image endpoint
resp = requests.get(url, auth=...)
if resp.status_code == 404:
    raise FileNotFoundError(filename)  # treat as inaccessible

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/a21fce4835fe9b26. Report an issue: GitHub.