Model chaining via base_model_id:
- backend/open_webui/routers/models.py (lines 170-214, create_new_model)
- backend/open_webui/routers/models.py (lines 254-308, import_models)
- backend/open_webui/main.py (lines 1696-1711, base model resolution in chat completion)
- backend/open_webui/routers/openai.py (lines 1032-1037, base model payload rewrite)
- backend/open_webui/routers/ollama.py (lines 1086-1090, base model payload rewrite)
- backend/open_webui/utils/models.py (line 380, check_model_access — checks user-facing model only)
Current main branch (commit 6fdd19bf1) and likely all versions with the model chaining (base_model_id) feature.
Open WebUI supports model composition via base_model_id: a user-defined model (e.g., "Cheap Assistant") can reference an existing base model (e.g., "gpt-4-turbo-restricted") that provides the actual inference capability. When a user queries the composed model, the access control pipeline verifies the user has access to the composed model but never re-verifies access to the chained base model.
Additionally, the model creation and import endpoints accept arbitrary base_model_id values without checking that the caller has access to that base model. Combined, this allows any user with the default model creation permission to create a model that chains to a restricted base model — and then invoke it, causing the server to dispatch the request to the restricted base model using the admin-configured API key.
# utils/models.py:380 — access check runs against the user-facing model only
def check_model_access(user, model):
if user.role == 'user':
...check access grants on `model`...
# main.py:1696-1711 — base model resolved without access check
base_model = request.app.state.MODELS.get(model.info.base_model_id)
if base_model:
# payload["model"] is rewritten to base_model.id
# but no check_model_access(user, base_model) is performed
# openai.py:1032-1037 / ollama.py:1086-1090 — the rewritten payload is dispatched
payload['model'] = base_model_id
gpt-4-turbo-restricted and configures access grants so only the "ML Engineers" group can use it.POST /api/v1/models/create
{
"id": "cheap-assistant",
"name": "Cheap Assistant",
"base_model_id": "gpt-4-turbo-restricted",
"params": {},
"meta": {}
}
The creation endpoint does not validate the attacker's access to gpt-4-turbo-restricted.cheap-assistant. check_model_access(attacker, cheap-assistant) passes trivially because they are the owner.POST /api/chat/completions
{"model": "cheap-assistant", "messages": [...]}
main.py:1696, the pipeline resolves cheap-assistant.base_model_id to gpt-4-turbo-restricted, rewrites payload["model"] to the base model ID, and dispatches the upstream request with the admin-configured API key for the backend.The same bypass is available via the import endpoint, which additionally allows overwriting existing models (see related finding on model import ownership).
gpt-4-turbo-restricted (or equivalent paid/tiered/internal models) becomes silently ineffectiveworkspace.models permission, granted to all users by default){
"github_reviewed": true,
"github_reviewed_at": "2026-05-08T19:45:03Z",
"cwe_ids": [
"CWE-862"
],
"severity": "HIGH",
"nvd_published_at": null
}