From c66c273f62a67802ad777cd3b7495cd624867575 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sun, 10 May 2026 17:59:08 +0200 Subject: [PATCH] fix: strip model params for read-only callers on per-id endpoint (#24525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/v1/models/model?id= at routers/models.py:412 returned the full model.model_dump() to any caller with read access, including the params dict that holds the admin-curated system prompt and other behavior config. The user-facing /api/models endpoint already strips this via utils/models.py:170,210 with the comment "Remove params to avoid exposing sensitive info", and /api/v1/models/list gates by write permission so non-curators don't see the model in their workspace listing at all. The per-id endpoint missed the same gate, so a user with read-only access (e.g. granted access to use the model in chat) could open /workspace/models/edit?id= in the browser and read the system prompt verbatim from the network response, even though saving was correctly blocked. Compute write_access once at the top of the handler so it can serve both the response-shape decision and the response field. When the caller lacks write access, replace params with an empty dict in the serialised response. Owners, admins under BYPASS_ADMIN_ACCESS_CONTROL, and explicit write-grant holders still get the full payload so the workspace edit UI keeps working for users who legitimately curate the model. Read-permission users continue to receive everything else they need to chat with the model — the chat path resolves prompt/params server-side from the stored ModelModel and never echoes them back through this endpoint. Reported by destination-one in GHSA-h2cw-7qw9-56xr. Co-authored-by: destination-one --- backend/open_webui/routers/models.py | 38 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 1ced11b358..cef4429760 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -413,9 +413,20 @@ class ModelIdForm(BaseModel): async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): model = await Models.get_model_by_id(id, db=db) if model: - if ( + write_access = ( (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) - or model.user_id == user.id + or user.id == model.user_id + or await AccessGrants.has_access( + user_id=user.id, + resource_type='model', + resource_id=model.id, + permission='write', + db=db, + ) + ) + + if ( + write_access or await AccessGrants.has_access( user_id=user.id, resource_type='model', @@ -424,19 +435,18 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes db=db, ) ): + model_dict = model.model_dump() + # Strip params (system prompt and other admin-curated config) + # for read-only callers — matches the params strip already + # enforced on /api/models in utils/models.py. Owners, admins + # under BYPASS_ADMIN_ACCESS_CONTROL, and write-grant holders + # still receive the full object so the workspace edit UI keeps + # working for users who legitimately curate the model. + if not write_access: + model_dict['params'] = {} return ModelAccessResponse( - **model.model_dump(), - write_access=( - (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) - or user.id == model.user_id - or await AccessGrants.has_access( - user_id=user.id, - resource_type='model', - resource_id=model.id, - permission='write', - db=db, - ) - ), + **model_dict, + write_access=write_access, ) else: raise HTTPException(