diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-06-24 22:47:13 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-06-24 22:47:13 +0900 |
| commit | e4d421c782a181f421e2987f75b4610845e3ef55 (patch) | |
| tree | e61b848b32dc046977751346d4346c4debc7c4f8 /src/ollama_web/routes/sessions.py | |
| parent | a1d744387659f7fe29559017ae03dc76e8532bb1 (diff) | |
Harden web UI security
Diffstat (limited to 'src/ollama_web/routes/sessions.py')
| -rw-r--r-- | src/ollama_web/routes/sessions.py | 51 |
1 files changed, 40 insertions, 11 deletions
diff --git a/src/ollama_web/routes/sessions.py b/src/ollama_web/routes/sessions.py index 8640ec3..7b01705 100644 --- a/src/ollama_web/routes/sessions.py +++ b/src/ollama_web/routes/sessions.py @@ -2,20 +2,24 @@ from __future__ import annotations +import json import logging -from typing import Any +from typing import Any, cast +from starlette.datastructures import UploadFile from starlette.requests import Request from starlette.responses import JSONResponse -from ..sessions import SessionStore, _IMAGE_MIMES, _IMAGE_SUFFIXES -from ..tools.pdf import extract_pdf_text +from ..sessions import _IMAGE_MIMES, _IMAGE_SUFFIXES, SessionStore, is_valid_id +from ..tools.helper.pdf import extract_pdf_text logger = logging.getLogger("ollama_web.sessions") +_UPLOAD_CHUNK_SIZE = 1024 * 1024 + def _get_store(request: Request) -> SessionStore: - return request.app.state.session_store + return cast(SessionStore, request.app.state.session_store) def _is_image_file(name: str, mime: str) -> bool: @@ -36,7 +40,8 @@ async def list_sessions(request: Request) -> JSONResponse: async def create_session(request: Request) -> JSONResponse: store = _get_store(request) - body = await request.json() if await request.body() else {} + raw = await request.body() + body = json.loads(raw.decode("utf-8")) if raw else {} title = body.get("title") session = store.create(title=title) return JSONResponse(session) @@ -45,6 +50,8 @@ async def create_session(request: Request) -> JSONResponse: async def get_session(request: Request) -> JSONResponse: store = _get_store(request) session_id = request.path_params["session_id"] + if not is_valid_id(session_id): + return JSONResponse({"error": "Invalid session id"}, status_code=400) session = store.get(session_id) if session is None: return JSONResponse({"error": "Session not found"}, status_code=404) @@ -54,6 +61,8 @@ async def get_session(request: Request) -> JSONResponse: async def delete_session(request: Request) -> JSONResponse: store = _get_store(request) session_id = request.path_params["session_id"] + if not is_valid_id(session_id): + return JSONResponse({"error": "Invalid session id"}, status_code=400) ok = store.delete(session_id) if not ok: return JSONResponse({"error": "Session not found"}, status_code=404) @@ -63,6 +72,8 @@ async def delete_session(request: Request) -> JSONResponse: async def upload_file(request: Request) -> JSONResponse: store = _get_store(request) session_id = request.path_params["session_id"] + if not is_valid_id(session_id): + return JSONResponse({"error": "Invalid session id"}, status_code=400) session = store.get(session_id) if session is None: return JSONResponse({"error": "Session not found"}, status_code=404) @@ -79,22 +90,38 @@ async def upload_file(request: Request) -> JSONResponse: for upload in files: if upload is None: continue + if not hasattr(upload, "read"): + results.append({"name": "unknown", "error": "Invalid upload"}) + continue + upload_file = cast(UploadFile, upload) try: - data = await upload.read() + chunks: list[bytes] = [] + total = 0 + while True: + chunk = await upload_file.read(_UPLOAD_CHUNK_SIZE) + if not chunk: + break + total += len(chunk) + if total > max_size: + raise ValueError( + f"File exceeds {request.app.state.settings.max_upload_mb} MB limit" + ) + chunks.append(chunk) + data = b"".join(chunks) except Exception as exc: # noqa: BLE001 - results.append({"name": getattr(upload, "filename", "unknown"), "error": str(exc)}) + results.append({"name": getattr(upload_file, "filename", "unknown"), "error": str(exc)}) continue if len(data) > max_size: results.append( { - "name": getattr(upload, "filename", "unknown"), + "name": getattr(upload_file, "filename", "unknown"), "error": f"File exceeds {request.app.state.settings.max_upload_mb} MB limit", } ) continue - name = getattr(upload, "filename", "uploaded-file") or "uploaded-file" - content_type = getattr(upload, "content_type", None) or "application/octet-stream" + name = getattr(upload_file, "filename", "uploaded-file") or "uploaded-file" + content_type = getattr(upload_file, "content_type", None) or "application/octet-stream" text = "" if _is_image_file(name, content_type): # Images are passed to ollama via Message.images (base64), not as text. @@ -128,7 +155,9 @@ async def delete_file(request: Request) -> JSONResponse: store = _get_store(request) session_id = request.path_params["session_id"] file_id = request.path_params["file_id"] + if not is_valid_id(session_id) or not is_valid_id(file_id): + return JSONResponse({"error": "Invalid id"}, status_code=400) ok = store.remove_file(session_id, file_id) if not ok: return JSONResponse({"error": "File not found"}, status_code=404) - return JSONResponse({"deleted": True})
\ No newline at end of file + return JSONResponse({"deleted": True}) |
