fix(gallery): confine replacement image path (#4285)

This commit is contained in:
RaresKeY
2026-06-15 17:42:41 +03:00
committed by GitHub
parent f66a23d19d
commit 81e7074d93
2 changed files with 60 additions and 5 deletions
+2 -5
View File
@@ -232,8 +232,6 @@ def setup_gallery_routes() -> APIRouter:
@router.post("/api/gallery/{image_id}/replace") @router.post("/api/gallery/{image_id}/replace")
async def gallery_replace(request: Request, image_id: str): async def gallery_replace(request: Request, image_id: str):
"""Replace an existing gallery image file with a new one.""" """Replace an existing gallery image file with a new one."""
from pathlib import Path
user = get_current_user(request) user = get_current_user(request)
db = SessionLocal() db = SessionLocal()
try: try:
@@ -249,9 +247,8 @@ def setup_gallery_routes() -> APIRouter:
raise HTTPException(400, "No image provided") raise HTTPException(400, "No image provided")
content = await read_upload_limited(file, GALLERY_UPLOAD_MAX_BYTES, "Gallery replacement") content = await read_upload_limited(file, GALLERY_UPLOAD_MAX_BYTES, "Gallery replacement")
img_dir = Path(GENERATED_IMAGES_DIR) GALLERY_IMAGE_DIR.mkdir(parents=True, exist_ok=True)
img_dir.mkdir(parents=True, exist_ok=True) img_path = _gallery_image_path(img.filename)
img_path = img_dir / _sanitize_gallery_filename(img.filename)
img_path.write_bytes(content) img_path.write_bytes(content)
# Refresh dimensions in case the editor resized the canvas. # Refresh dimensions in case the editor resized the canvas.
@@ -2,7 +2,14 @@ import os
from pathlib import Path from pathlib import Path
import pytest import pytest
from fastapi import FastAPI
from fastapi import HTTPException 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(): def _gallery_module():
@@ -53,6 +60,57 @@ def test_gallery_image_path_rejects_symlink_escape(tmp_path, monkeypatch):
assert exc.value.status_code == 400 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(): def test_gallery_file_operations_use_confining_resolver():
source = Path("routes/gallery_routes.py").read_text(encoding="utf-8") source = Path("routes/gallery_routes.py").read_text(encoding="utf-8")