From 6fc0830d2ca7daf2631eedb267ed20dbe1af097f Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Wed, 1 Jul 2026 11:47:29 +0200 Subject: [PATCH] api audit Signed-off-by: Vladimir Mandic --- CHANGELOG.md | 29 +++++++++++++++-------------- modules/api/api.py | 12 ++++++++++-- modules/api/endpoints.py | 15 ++++++++++++++- modules/api/server.py | 7 +++++++ 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41e075063..14012b905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,27 +39,28 @@ And **SDNQ** improvements: now with *NPU* support and its own native *attention* - **Internal** - delay init of video models - **Fixes** - - sdnq: warn instead of error for triton + - amd: hipBLASLt improved detection, thanks @0xDELUXA + - api: add missing endpoint registration + - api: openapi schema exposure + - api: stricter api request and response schemas + - caption: button in standard-ui + - embeddings: handle textual-inversion with new transformers - insightface: missing dependencies - - pulid: import paths - - processors: init code and multiple fixes - live preview: configurable pause when not in focus, thanks @Artheriax + - log: strip ansi sequences from ring buffer and client side logging + - measure: handle current kanvas stage + - model metadata: handle invalid metadata and strip workflows - mps: install `torchsde` as requirement - - vae: scale factor improved detection - - ui: networks details scrollbars - onnxruntime: handle invalid version - onnxruntime: mark all import paths as non-critical - - measure: handle current kanvas stage - - caption: button in standard-ui - - python: experimental/ignore version checks - - hipBLASLt: improved detection, thanks @0xDELUXA - - embeddings: handle textual-inversion with new transformers - options: handle compatibility options - - log: strip ansi sequences from ring buffer and client side logging - - model metadata: handle invalid metadata and strip workflows + - processors: init code and multiple fixes + - pulid: import paths + - python: experimental/ignore version checks + - sdnq: warn instead of error for `triton` - ui debounce aspect-ratio linked width/height controls - - api: stricter api request and response schemas - - api: openapi schema exposure + - ui: networks details scrollbars + - vae: scale factor improved detection ## Update for 2026-06-16 diff --git a/modules/api/api.py b/modules/api/api.py index 86c67ae84..990a769d5 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -64,7 +64,9 @@ class Api: self.add_api_route("/sdapi/v1/txt2img", self.generate.post_text2img, methods=["POST"], response_model=models.ResTxt2Img, tags=["Generation"]) self.add_api_route("/sdapi/v1/img2img", self.generate.post_img2img, methods=["POST"], response_model=models.ResImg2Img, tags=["Generation"]) self.add_api_route("/sdapi/v1/control", self.control.post_control, methods=["POST"], response_model=control.ResControl, tags=["Generation"]) + self.add_api_route("/sdapi/v1/process", self.process.extras_single_image_api, methods=["POST"], response_model=models.ResProcessImage, tags=["Processing"]) self.add_api_route("/sdapi/v1/extra-single-image", self.process.extras_single_image_api, methods=["POST"], response_model=models.ResProcessImage, tags=["Processing"]) + self.add_api_route("/sdapi/v1/process-batch", self.process.extras_batch_images_api, methods=["POST"], response_model=models.ResProcessBatch, tags=["Processing"]) self.add_api_route("/sdapi/v1/extra-batch-images", self.process.extras_batch_images_api, methods=["POST"], response_model=models.ResProcessBatch, tags=["Processing"]) self.add_api_route("/sdapi/v1/preprocess", self.process.post_preprocess, methods=["POST"], tags=["Processing"]) self.add_api_route("/sdapi/v1/mask", self.process.post_mask, methods=["POST"], tags=["Processing"]) @@ -80,23 +82,29 @@ class Api: self.add_api_route("/sdapi/v1/preprocessors", self.process.get_preprocess, methods=["GET"], response_model=list[process.ItemPreprocess]) self.add_api_route("/sdapi/v1/masking", self.process.get_mask, methods=["GET"], response_model=process.ItemMask) self.add_api_route("/sdapi/v1/samplers", endpoints.get_samplers, methods=["GET"], response_model=list[models.ItemSampler]) + self.add_api_route("/sdapi/v1/schedulers", endpoints.get_schedulers, methods=["GET"], response_model=list[models.ItemScheduler]) self.add_api_route("/sdapi/v1/upscalers", endpoints.get_upscalers, methods=["GET"], response_model=list[models.ItemUpscaler]) self.add_api_route("/sdapi/v1/sd-models", endpoints.get_sd_models, methods=["GET"], response_model=list[models.ItemModel]) self.add_api_route("/sdapi/v1/controlnets", endpoints.get_controlnets, methods=["GET"], response_model=list[str]) + self.add_api_route("/sdapi/v1/control-models", endpoints.get_control_models, methods=["GET"], response_model=list[str]) + self.add_api_route("/sdapi/v1/control-modes", endpoints.get_control_modes, methods=["GET"], response_model=dict[str, list[str]]) self.add_api_route("/sdapi/v1/face-restorers", endpoints.get_restorers, methods=["GET"], response_model=list[models.ItemDetailer]) self.add_api_route("/sdapi/v1/detailers", endpoints.get_detailers, methods=["GET"], response_model=list[models.ItemDetailer]) self.add_api_route("/sdapi/v1/prompt-styles", endpoints.get_prompt_styles, methods=["GET"], response_model=list[models.ItemStyle]) self.add_api_route("/sdapi/v1/embeddings", endpoints.get_embeddings, methods=["GET"], response_model=models.ResEmbeddings) + self.add_api_route("/sdapi/v1/ip-adapters", endpoints.get_ip_adapters, methods=["GET"], response_model=list[str]) self.add_api_route("/sdapi/v1/wildcards", endpoints.get_wildcards, methods=["GET"], response_model=list[dict], tags=["Enumerators"]) self.add_api_route("/sdapi/v1/sd-vae", endpoints.get_sd_vaes, methods=["GET"], response_model=list[models.ItemVae]) self.add_api_route("/sdapi/v1/extensions", endpoints.get_extensions_list, methods=["GET"], response_model=list[models.ItemExtension]) self.add_api_route("/sdapi/v1/extra-networks", endpoints.get_extra_networks, methods=["GET"], response_model=list[models.ItemExtraNetwork]) + self.add_api_route("/sdapi/v1/extra-network-detail", endpoints.get_extra_network_detail, methods=["GET"], response_model=models.ItemExtraNetworkFull) + self.add_api_route("/sdapi/v1/extra-network-details", endpoints.get_extra_network_details, methods=["GET"], response_model=models.ResExtraNetworkDetails) self.add_api_route("/sdapi/v1/unets", endpoints.get_unets, methods=["GET"], response_model=list[models.ItemUNet]) # functional api self.add_api_route("/sdapi/v1/file", endpoints.get_file, methods=["GET"], tags=["Functional"]) - self.add_api_route("/sdapi/v1/delete-image", endpoints.get_deleteimage, methods=["GET"], tags=["Functional"]) - self.add_api_route("/sdapi/v1/delete-file", endpoints.get_deletefile, methods=["GET"], tags=["Functional"]) + self.add_api_route("/sdapi/v1/delete-image", endpoints.get_deleteimage, methods=["DELETE"], tags=["Functional"]) + self.add_api_route("/sdapi/v1/delete-file", endpoints.get_deletefile, methods=["DELETE"], tags=["Functional"]) self.add_api_route("/sdapi/v1/png-info", endpoints.get_pnginfo, methods=["GET"], response_model=models.ResImageInfo, tags=["Functional"]) self.add_api_route("/sdapi/v1/png-info", endpoints.post_pnginfo, methods=["POST"], response_model=models.ResImageInfo, tags=["Functional"]) self.add_api_route("/sdapi/v1/checkpoint", endpoints.get_checkpoint, methods=["GET"], tags=["Functional"]) diff --git a/modules/api/endpoints.py b/modules/api/endpoints.py index 06e6fd543..48d4c59c5 100644 --- a/modules/api/endpoints.py +++ b/modules/api/endpoints.py @@ -210,7 +210,14 @@ def get_schedulers(): """List all available schedulers with their class names and options.""" from modules.sd_samplers import list_samplers all_schedulers = list_samplers() - return all_schedulers + return [ + { + 'name': scheduler.name, + 'cls': scheduler.constructor.__name__ if scheduler.constructor is not None else None, + 'options': scheduler.options, + } + for scheduler in all_schedulers + ] def post_unload_checkpoint(): """Unload the current model and refiner from memory to free VRAM.""" @@ -331,6 +338,8 @@ def get_file(file: str): import os from pathlib import Path from starlette.responses import FileResponse + if shared.demo is None: + raise HTTPException(status_code=503, detail="server not ready") allowed_dirs = shared.demo.allowed_paths if not file.strip(): raise HTTPException(status_code=400, detail="file path is required") @@ -345,6 +354,8 @@ def get_file(file: str): def get_deletefile(file: str): import os from pathlib import Path + if shared.demo is None: + raise HTTPException(status_code=503, detail="server not ready") allowed_dirs = shared.demo.allowed_paths if file is None or len(file.strip()) == 0: raise HTTPException(status_code=400, detail="file path is required") @@ -368,6 +379,8 @@ def get_deletefile(file: str): def get_deleteimage(file: str): import os from pathlib import Path + if shared.demo is None: + raise HTTPException(status_code=503, detail="server not ready") allowed_dirs = shared.demo.allowed_paths if file is None or len(file.strip()) == 0: raise HTTPException(status_code=400, detail="file path is required") diff --git a/modules/api/server.py b/modules/api/server.py index 0f96cdf87..8d4fe9b14 100644 --- a/modules/api/server.py +++ b/modules/api/server.py @@ -1,5 +1,6 @@ import os import time +from pathlib import Path from fastapi import Request, Depends, BackgroundTasks, Response from fastapi.exceptions import HTTPException from fastapi.responses import FileResponse @@ -13,6 +14,12 @@ def get_js(request: Request): file = request.query_params.get("file", None) if (file is None) or (len(file) == 0): raise HTTPException(status_code=400, detail="file parameter is required") + # Security: validate path is within allowed directories + if shared.demo is None: + raise HTTPException(status_code=503, detail="server not ready") + allowed_dirs = shared.demo.allowed_paths + if not any(Path(folder).absolute() in Path(file).absolute().parents for folder in allowed_dirs): + raise HTTPException(status_code=403, detail=f"file {file}: must be in one of allowed directories") ext = file.split('.')[-1] if ext not in ['js', 'css', 'map', 'html', 'wasm', 'ttf', 'mjs', 'json']: raise HTTPException(status_code=400, detail=f"invalid file extension: {ext}")