diff --git a/routes/gallery_routes.py b/routes/gallery_routes.py index 239281ac1..808f8488e 100644 --- a/routes/gallery_routes.py +++ b/routes/gallery_routes.py @@ -232,8 +232,6 @@ def setup_gallery_routes() -> APIRouter: @router.post("/api/gallery/{image_id}/replace") async def gallery_replace(request: Request, image_id: str): """Replace an existing gallery image file with a new one.""" - from pathlib import Path - user = get_current_user(request) db = SessionLocal() try: @@ -249,9 +247,8 @@ def setup_gallery_routes() -> APIRouter: raise HTTPException(400, "No image provided") content = await read_upload_limited(file, GALLERY_UPLOAD_MAX_BYTES, "Gallery replacement") - img_dir = Path(GENERATED_IMAGES_DIR) - img_dir.mkdir(parents=True, exist_ok=True) - img_path = img_dir / _sanitize_gallery_filename(img.filename) + GALLERY_IMAGE_DIR.mkdir(parents=True, exist_ok=True) + img_path = _gallery_image_path(img.filename) img_path.write_bytes(content) # Refresh dimensions in case the editor resized the canvas. diff --git a/tests/test_gallery_filename_confinement.py b/tests/test_gallery_filename_confinement.py index 5e6c3f051..5bed85fe4 100644 --- a/tests/test_gallery_filename_confinement.py +++ b/tests/test_gallery_filename_confinement.py @@ -2,7 +2,14 @@ import os from pathlib import Path import pytest +from fastapi import FastAPI from fastapi import HTTPException +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool + +from core.database import Base, GalleryImage def _gallery_module(): @@ -53,6 +60,57 @@ def test_gallery_image_path_rejects_symlink_escape(tmp_path, monkeypatch): assert exc.value.status_code == 400 +def test_gallery_replace_rejects_symlink_escape(tmp_path, monkeypatch): + gallery_routes = _gallery_module() + image_dir = tmp_path / "generated_images" + image_dir.mkdir() + outside = tmp_path / "outside.png" + outside.write_bytes(b"outside image root") + link = image_dir / "escape.png" + try: + os.symlink(outside, link) + except (AttributeError, NotImplementedError, OSError) as exc: + pytest.skip(f"symlinks unavailable: {exc}") + + engine = create_engine( + f"sqlite:///{tmp_path / 'gallery.db'}", + connect_args={"check_same_thread": False}, + poolclass=NullPool, + ) + Base.metadata.create_all(engine) + SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) + db = SessionLocal() + try: + db.add( + GalleryImage( + id="img-1", + filename="escape.png", + prompt="escape", + owner="alice", + is_active=True, + ) + ) + db.commit() + finally: + db.close() + + monkeypatch.setattr(gallery_routes, "GALLERY_IMAGE_DIR", image_dir) + monkeypatch.setattr(gallery_routes, "SessionLocal", SessionLocal) + monkeypatch.setattr(gallery_routes, "get_current_user", lambda request: "alice") + + app = FastAPI() + app.include_router(gallery_routes.setup_gallery_routes()) + client = TestClient(app) + + response = client.post( + "/api/gallery/img-1/replace", + files={"image": ("replacement.png", b"replacement bytes", "image/png")}, + ) + + assert response.status_code == 400 + assert outside.read_bytes() == b"outside image root" + + def test_gallery_file_operations_use_confining_resolver(): source = Path("routes/gallery_routes.py").read_text(encoding="utf-8")