mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 03:12:21 +02:00
* 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>
73 lines
2.5 KiB
Python
73 lines
2.5 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
|
|
def _generated_images_module():
|
|
from src import generated_images
|
|
return generated_images
|
|
|
|
|
|
def test_generated_image_path_allows_safe_existing_file(tmp_path, monkeypatch):
|
|
generated_images = _generated_images_module()
|
|
image_dir = tmp_path / "generated_images"
|
|
image_dir.mkdir()
|
|
filename = "a" * 12 + ".png"
|
|
image_path = image_dir / filename
|
|
image_path.write_bytes(b"png")
|
|
monkeypatch.setattr(generated_images, "GENERATED_IMAGE_DIR", image_dir)
|
|
|
|
assert generated_images.resolve_generated_image_path(filename) == image_path
|
|
|
|
|
|
@pytest.mark.parametrize("filename", ["../../secret.png", "zzzzzzzz.png", "aaaaaaa.png", None, 12345])
|
|
def test_generated_image_path_rejects_invalid_filenames(tmp_path, monkeypatch, filename):
|
|
generated_images = _generated_images_module()
|
|
image_dir = tmp_path / "generated_images"
|
|
image_dir.mkdir()
|
|
monkeypatch.setattr(generated_images, "GENERATED_IMAGE_DIR", image_dir)
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
generated_images.resolve_generated_image_path(filename)
|
|
|
|
assert exc.value.status_code == 400
|
|
|
|
|
|
def test_generated_image_path_rejects_symlink_escape(tmp_path, monkeypatch):
|
|
generated_images = _generated_images_module()
|
|
image_dir = tmp_path / "generated_images"
|
|
image_dir.mkdir()
|
|
filename = "b" * 12 + ".png"
|
|
outside = tmp_path / "outside.png"
|
|
outside.write_bytes(b"outside image root")
|
|
try:
|
|
os.symlink(outside, image_dir / filename)
|
|
except (AttributeError, NotImplementedError, OSError) as exc:
|
|
pytest.skip(f"symlinks unavailable: {exc}")
|
|
monkeypatch.setattr(generated_images, "GENERATED_IMAGE_DIR", image_dir)
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
generated_images.resolve_generated_image_path(filename)
|
|
|
|
assert exc.value.status_code == 400
|
|
|
|
|
|
def test_generated_image_headers_include_nosniff():
|
|
generated_images = _generated_images_module()
|
|
|
|
assert generated_images.GENERATED_IMAGE_HEADERS["X-Content-Type-Options"] == "nosniff"
|
|
assert (
|
|
generated_images.GENERATED_IMAGE_HEADERS["Cache-Control"]
|
|
== "private, no-cache, must-revalidate"
|
|
)
|
|
|
|
|
|
def test_generated_image_route_uses_confining_resolver():
|
|
source = Path("app.py").read_text(encoding="utf-8")
|
|
|
|
assert 'Path("data/generated_images") / filename' not in source
|
|
assert "resolve_generated_image_path(filename)" in source
|
|
assert "headers=GENERATED_IMAGE_HEADERS" in source
|