fix(security): deny generated-image access when ownership verification fails (#6268)

* fix(security): deny generated-image access when ownership verification fails

The except Exception block in serve_generated_image catches all
exceptions during ownership verification and falls through to serve
the file anyway. If the database is down or corrupted, the ownership
check is bypassed entirely — any authenticated user can download any
generated image by guessing the content-hash filename.

Raise HTTP 503 (Service Unavailable) instead of falling through,
so the endpoint fails closed when verification is impossible. This
matches the defense-in-depth principle: deny access on verification
errors rather than granting it.

* fix(security): enforce fail-closed ownership and secure caching for generated images

- Fail closed on missing gallery records, null/empty owners, and DB errors
- Support both cookie session and bearer API token owners via effective_user
- Add in-memory registry for freshly generated images to preserve access
- Eliminate file existence oracle by verifying ownership before disk probing
- Update Cache-Control to private, no-cache, must-revalidate
- Pass owner attribution through tool execution and image MCP server
- Add comprehensive hermetic regression tests for ownership gates and caching

* fix(security): sanitize tool owner arguments and drop unneeded MCP env fallback

- Remove redundant and unsafe ODYSSEUS_MCP_IMAGE_OWNER fallback in image_gen_server.py to fix 4 tests in test_env_reference.py.
- Sanitize caller-supplied tool arguments across all MCP and email tool dispatch paths in src/tool_execution.py: strip untrusted _odysseus_* keys from arguments.
- Only inject _odysseus_owner when verified trusted server context is present, preventing untrusted/anonymous users from spoofing ownership claims.
- Sanitize empty/whitespace owner strings in image_gen_server.py to None.
- Add comprehensive regression tests in test_generated_image_ownership.py for argument sanitization, owner injection, fallback propagation, and server sanitization.

---------

Co-authored-by: isharak7m <192635824+isharak7m@users.noreply.github.com>
Co-authored-by: PR #6166 Reviewer <reviewer@odysseus-review.local>
This commit is contained in:
Isharak
2026-10-14 12:18:50 +01:00
committed by GitHub
co-authored by isharak7m PR #6166 Reviewer
parent 501059805f
commit 6fceb6ccd3
7 changed files with 643 additions and 39 deletions
+9
View File
@@ -1372,6 +1372,9 @@ async def do_generate_image(content: str, session_id: Optional[str] = None, owne
filename = f"{uuid.uuid4().hex[:12]}.png"
img_path = img_dir / filename
img_path.write_bytes(base64.b64decode(img.get("b64_json")))
if owner:
from src.generated_images import register_generated_image
register_generated_image(filename, owner)
image_url = f"/api/generated-image/{filename}"
image_id = _save_to_gallery(filename)
@@ -1392,6 +1395,9 @@ async def do_generate_image(content: str, session_id: Optional[str] = None, owne
filename = f"{uuid.uuid4().hex[:12]}.png"
img_path = img_dir / filename
img_path.write_bytes(dl_resp.content)
if owner:
from src.generated_images import register_generated_image
register_generated_image(filename, owner)
image_url = f"/api/generated-image/{filename}"
image_id = _save_to_gallery(filename)
else:
@@ -1537,6 +1543,9 @@ async def do_edit_image(
img_dir.mkdir(parents=True, exist_ok=True)
filename = f"{uuid.uuid4().hex[:12]}{suffix}"
(img_dir / filename).write_bytes(image_bytes)
if owner:
from src.generated_images import register_generated_image
register_generated_image(filename, owner)
return f"/api/generated-image/{filename}", _save_edited_image_to_gallery(filename)
async def _try_local_img2img_fallback(client: httpx.AsyncClient) -> Optional[Dict[str, Any]]: