fix: strip model params for read-only callers on per-id endpoint (#24525)

GET /api/v1/models/model?id=<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=<not-mine> 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 <destination-one@users.noreply.github.com>
This commit is contained in:
Classic298
2026-05-10 17:59:08 +02:00
committed by GitHub
parent e7ba8978c6
commit c66c273f62

View File

@@ -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(