From a428806cb382d475e144cd49af58f803c8b0be78 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 24 Aug 2026 17:44:05 +0100 Subject: [PATCH 1/5] chore: annotate validated attachment paths Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 104d342a-090c-4feb-9bb3-662915b9c390 --- python/samples/05-end-to-end/chatkit-integration/app.py | 6 +++--- .../05-end-to-end/chatkit-integration/attachment_store.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/samples/05-end-to-end/chatkit-integration/app.py b/python/samples/05-end-to-end/chatkit-integration/app.py index 612d9f9c443..ecce054dd6a 100644 --- a/python/samples/05-end-to-end/chatkit-integration/app.py +++ b/python/samples/05-end-to-end/chatkit-integration/app.py @@ -600,7 +600,7 @@ async def upload_file(attachment_id: str, file: UploadFile = File(...)): # noqa contents = await file.read() # Save to disk - file_path.write_bytes(contents) + file_path.write_bytes(contents) # codeql[py/path-injection] Path is constrained by get_file_path. logger.info(f"Saved {len(contents)} bytes to {file_path}") @@ -637,7 +637,7 @@ async def preview_image(attachment_id: str): return JSONResponse(status_code=400, content={"error": "Invalid attachment ID."}) try: - if not file_path.exists(): + if not file_path.exists(): # codeql[py/path-injection] Path is constrained by get_file_path. return JSONResponse(status_code=404, content={"error": "File not found"}) # Determine media type from file extension or attachment metadata @@ -649,7 +649,7 @@ async def preview_image(attachment_id: str): # Default to binary if we can't determine media_type = "application/octet-stream" - return FileResponse(file_path, media_type=media_type) + return FileResponse(file_path, media_type=media_type) # codeql[py/path-injection] Path is constrained by get_file_path. except Exception as e: logger.error(f"Error serving preview for attachment {attachment_id}: {e}", exc_info=True) diff --git a/python/samples/05-end-to-end/chatkit-integration/attachment_store.py b/python/samples/05-end-to-end/chatkit-integration/attachment_store.py index b08ae9c43a4..cd139401925 100644 --- a/python/samples/05-end-to-end/chatkit-integration/attachment_store.py +++ b/python/samples/05-end-to-end/chatkit-integration/attachment_store.py @@ -70,7 +70,7 @@ def get_file_path(self, attachment_id: str) -> Path: if not attachment_id or attachment_id in {".", ".."} or "/" in attachment_id or "\\" in attachment_id: raise ValueError(f"Invalid attachment ID: {attachment_id!r}") - file_path = (self.uploads_dir / attachment_id).resolve() + file_path = (self.uploads_dir / attachment_id).resolve() # codeql[py/path-injection] Path containment is validated below. if not file_path.is_relative_to(self.uploads_dir) or file_path.parent != self.uploads_dir: raise ValueError(f"Invalid attachment ID: {attachment_id!r}") return file_path From 67c7eb083e050f9ee17ff77046597675d4b226c0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 24 Aug 2026 17:53:49 +0100 Subject: [PATCH 2/5] fix: update attachment handling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 104d342a-090c-4feb-9bb3-662915b9c390 --- .../05-end-to-end/chatkit-integration/app.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/python/samples/05-end-to-end/chatkit-integration/app.py b/python/samples/05-end-to-end/chatkit-integration/app.py index ecce054dd6a..ce33099c608 100644 --- a/python/samples/05-end-to-end/chatkit-integration/app.py +++ b/python/samples/05-end-to-end/chatkit-integration/app.py @@ -43,7 +43,7 @@ # ChatKit imports from chatkit.actions import Action from chatkit.server import ChatKitServer -from chatkit.store import StoreItemType, default_generate_id +from chatkit.store import NotFoundError, StoreItemType, default_generate_id from chatkit.types import ( ThreadItem, ThreadItemDoneEvent, @@ -595,6 +595,14 @@ async def upload_file(attachment_id: str, file: UploadFile = File(...)): # noqa logger.warning(f"Rejected invalid attachment ID: {attachment_id!r}") return JSONResponse(status_code=400, content={"error": "Invalid attachment ID."}) + try: + attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID}) + except NotFoundError: + return JSONResponse(status_code=404, content={"error": "Attachment not found."}) + + if attachment.upload_url is None: + return JSONResponse(status_code=409, content={"error": "Attachment upload is already complete."}) + try: # Read file contents contents = await file.read() @@ -604,9 +612,6 @@ async def upload_file(attachment_id: str, file: UploadFile = File(...)): # noqa logger.info(f"Saved {len(contents)} bytes to {file_path}") - # Load the attachment metadata from the data store - attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID}) - # Clear the upload_url since upload is complete attachment.upload_url = None @@ -636,20 +641,16 @@ async def preview_image(attachment_id: str): logger.warning(f"Rejected invalid attachment ID: {attachment_id!r}") return JSONResponse(status_code=400, content={"error": "Invalid attachment ID."}) + try: + attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID}) + except NotFoundError: + return JSONResponse(status_code=404, content={"error": "Attachment not found."}) + try: if not file_path.exists(): # codeql[py/path-injection] Path is constrained by get_file_path. return JSONResponse(status_code=404, content={"error": "File not found"}) - # Determine media type from file extension or attachment metadata - # For simplicity, we'll try to load from the store - try: - attachment = await data_store.load_attachment(attachment_id, {"user_id": DEFAULT_USER_ID}) - media_type = attachment.mime_type - except Exception: - # Default to binary if we can't determine - media_type = "application/octet-stream" - - return FileResponse(file_path, media_type=media_type) # codeql[py/path-injection] Path is constrained by get_file_path. + return FileResponse(file_path, media_type=attachment.mime_type) # codeql[py/path-injection] Path is constrained by get_file_path. except Exception as e: logger.error(f"Error serving preview for attachment {attachment_id}: {e}", exc_info=True) From 524c77a7cd3b933860abbd87f3a82bb164e6434c Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 24 Aug 2026 18:07:31 +0100 Subject: [PATCH 3/5] chore: preserve analysis annotations Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 104d342a-090c-4feb-9bb3-662915b9c390 --- python/samples/05-end-to-end/chatkit-integration/app.py | 2 +- .../05-end-to-end/chatkit-integration/attachment_store.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/samples/05-end-to-end/chatkit-integration/app.py b/python/samples/05-end-to-end/chatkit-integration/app.py index ce33099c608..196d1888ce6 100644 --- a/python/samples/05-end-to-end/chatkit-integration/app.py +++ b/python/samples/05-end-to-end/chatkit-integration/app.py @@ -650,7 +650,7 @@ async def preview_image(attachment_id: str): if not file_path.exists(): # codeql[py/path-injection] Path is constrained by get_file_path. return JSONResponse(status_code=404, content={"error": "File not found"}) - return FileResponse(file_path, media_type=attachment.mime_type) # codeql[py/path-injection] Path is constrained by get_file_path. + return FileResponse(file_path, media_type=attachment.mime_type) # codeql[py/path-injection] Path is constrained by get_file_path. # fmt: skip except Exception as e: logger.error(f"Error serving preview for attachment {attachment_id}: {e}", exc_info=True) diff --git a/python/samples/05-end-to-end/chatkit-integration/attachment_store.py b/python/samples/05-end-to-end/chatkit-integration/attachment_store.py index cd139401925..cfdc0047777 100644 --- a/python/samples/05-end-to-end/chatkit-integration/attachment_store.py +++ b/python/samples/05-end-to-end/chatkit-integration/attachment_store.py @@ -70,7 +70,7 @@ def get_file_path(self, attachment_id: str) -> Path: if not attachment_id or attachment_id in {".", ".."} or "/" in attachment_id or "\\" in attachment_id: raise ValueError(f"Invalid attachment ID: {attachment_id!r}") - file_path = (self.uploads_dir / attachment_id).resolve() # codeql[py/path-injection] Path containment is validated below. + file_path = (self.uploads_dir / attachment_id).resolve() # codeql[py/path-injection] Path containment is validated below. # fmt: skip if not file_path.is_relative_to(self.uploads_dir) or file_path.parent != self.uploads_dir: raise ValueError(f"Invalid attachment ID: {attachment_id!r}") return file_path From d1c8648998933b3917022144cacd6141053d5df3 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Mon, 24 Aug 2026 19:22:49 +0100 Subject: [PATCH 4/5] fix: use attachment upload descriptor Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 104d342a-090c-4feb-9bb3-662915b9c390 --- .../05-end-to-end/chatkit-integration/app.py | 6 +++--- .../chatkit-integration/attachment_store.py | 19 ++++++++++++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/python/samples/05-end-to-end/chatkit-integration/app.py b/python/samples/05-end-to-end/chatkit-integration/app.py index 196d1888ce6..a70da3600ef 100644 --- a/python/samples/05-end-to-end/chatkit-integration/app.py +++ b/python/samples/05-end-to-end/chatkit-integration/app.py @@ -600,7 +600,7 @@ async def upload_file(attachment_id: str, file: UploadFile = File(...)): # noqa except NotFoundError: return JSONResponse(status_code=404, content={"error": "Attachment not found."}) - if attachment.upload_url is None: + if attachment.upload_descriptor is None: return JSONResponse(status_code=409, content={"error": "Attachment upload is already complete."}) try: @@ -612,8 +612,8 @@ async def upload_file(attachment_id: str, file: UploadFile = File(...)): # noqa logger.info(f"Saved {len(contents)} bytes to {file_path}") - # Clear the upload_url since upload is complete - attachment.upload_url = None + # Clear the upload descriptor since upload is complete + attachment.upload_descriptor = None # Save the updated attachment back to the store await data_store.save_attachment(attachment, {"user_id": DEFAULT_USER_ID}) diff --git a/python/samples/05-end-to-end/chatkit-integration/attachment_store.py b/python/samples/05-end-to-end/chatkit-integration/attachment_store.py index cfdc0047777..2e5a888801e 100644 --- a/python/samples/05-end-to-end/chatkit-integration/attachment_store.py +++ b/python/samples/05-end-to-end/chatkit-integration/attachment_store.py @@ -11,7 +11,13 @@ from typing import TYPE_CHECKING, Any from chatkit.store import AttachmentStore -from chatkit.types import Attachment, AttachmentCreateParams, FileAttachment, ImageAttachment +from chatkit.types import ( + Attachment, + AttachmentCreateParams, + AttachmentUploadDescriptor, + FileAttachment, + ImageAttachment, +) from pydantic import AnyUrl if TYPE_CHECKING: @@ -90,8 +96,11 @@ async def create_attachment(self, input: AttachmentCreateParams, context: dict[s # Generate unique ID for this attachment attachment_id = self.generate_attachment_id(input.mime_type, context) - # Generate upload URL that points to our FastAPI upload endpoint - upload_url = f"{self.base_url}/upload/{attachment_id}" + # Generate upload instructions that point to our FastAPI upload endpoint + upload_descriptor = AttachmentUploadDescriptor( + url=AnyUrl(f"{self.base_url}/upload/{attachment_id}"), + method="POST", + ) # Create appropriate attachment type based on MIME type if input.mime_type.startswith("image/"): @@ -103,7 +112,7 @@ async def create_attachment(self, input: AttachmentCreateParams, context: dict[s type="image", mime_type=input.mime_type, name=input.name, - upload_url=AnyUrl(upload_url), + upload_descriptor=upload_descriptor, preview_url=AnyUrl(preview_url), ) else: @@ -113,7 +122,7 @@ async def create_attachment(self, input: AttachmentCreateParams, context: dict[s type="file", mime_type=input.mime_type, name=input.name, - upload_url=AnyUrl(upload_url), + upload_descriptor=upload_descriptor, ) # Save attachment metadata to data store so it's available during upload From b61ca0716d8e4f5d3de4b63f421bd0042fe750e6 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Tue, 25 Aug 2026 10:00:51 +0100 Subject: [PATCH 5/5] chore: correct analysis annotations Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 104d342a-090c-4feb-9bb3-662915b9c390 --- python/samples/05-end-to-end/chatkit-integration/app.py | 6 +++--- .../05-end-to-end/chatkit-integration/attachment_store.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/samples/05-end-to-end/chatkit-integration/app.py b/python/samples/05-end-to-end/chatkit-integration/app.py index a70da3600ef..97097cc3945 100644 --- a/python/samples/05-end-to-end/chatkit-integration/app.py +++ b/python/samples/05-end-to-end/chatkit-integration/app.py @@ -608,7 +608,7 @@ async def upload_file(attachment_id: str, file: UploadFile = File(...)): # noqa contents = await file.read() # Save to disk - file_path.write_bytes(contents) # codeql[py/path-injection] Path is constrained by get_file_path. + file_path.write_bytes(contents) # CodeQL [SM01305] Path is constrained by get_file_path. logger.info(f"Saved {len(contents)} bytes to {file_path}") @@ -647,10 +647,10 @@ async def preview_image(attachment_id: str): return JSONResponse(status_code=404, content={"error": "Attachment not found."}) try: - if not file_path.exists(): # codeql[py/path-injection] Path is constrained by get_file_path. + if not file_path.exists(): # CodeQL [SM01305] Path is constrained by get_file_path. return JSONResponse(status_code=404, content={"error": "File not found"}) - return FileResponse(file_path, media_type=attachment.mime_type) # codeql[py/path-injection] Path is constrained by get_file_path. # fmt: skip + return FileResponse(file_path, media_type=attachment.mime_type) # CodeQL [SM01305] Path is constrained by get_file_path. # fmt: skip except Exception as e: logger.error(f"Error serving preview for attachment {attachment_id}: {e}", exc_info=True) diff --git a/python/samples/05-end-to-end/chatkit-integration/attachment_store.py b/python/samples/05-end-to-end/chatkit-integration/attachment_store.py index 2e5a888801e..c4de028b29a 100644 --- a/python/samples/05-end-to-end/chatkit-integration/attachment_store.py +++ b/python/samples/05-end-to-end/chatkit-integration/attachment_store.py @@ -76,7 +76,7 @@ def get_file_path(self, attachment_id: str) -> Path: if not attachment_id or attachment_id in {".", ".."} or "/" in attachment_id or "\\" in attachment_id: raise ValueError(f"Invalid attachment ID: {attachment_id!r}") - file_path = (self.uploads_dir / attachment_id).resolve() # codeql[py/path-injection] Path containment is validated below. # fmt: skip + file_path = (self.uploads_dir / attachment_id).resolve() # CodeQL [SM01305] Path containment is validated below. # fmt: skip if not file_path.is_relative_to(self.uploads_dir) or file_path.parent != self.uploads_dir: raise ValueError(f"Invalid attachment ID: {attachment_id!r}") return file_path