mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-28 05:27:35 +00:00
fix: reject skill IDs that are not URL path safe (#27660)
A skill ID goes straight into the path of every mutating skill endpoint (/api/v1/skills/id/{id}/...), but create only replaced spaces with hyphens. An ID containing a "/" was stored verbatim as the primary key, so the route never matched, the request fell through to the SPA static mount and the client got 405 Method Not Allowed. The skill could not be opened, edited, toggled or deleted, by admins either, and since skill.name is UNIQUE it could not be recreated under a corrected ID. Percent-encoding does not help: uvicorn decodes the path before Starlette routes it, so the only remaining fix was a direct database write.
Create now rejects any ID outside [a-z0-9_-] with 400 instead of silently storing an unreachable one. Two frontend paths that fed unsanitized IDs into it are fixed as well: the manual "Skill ID" field, which was bound with no sanitization at all and is the path that reproduces on every version, and the markdown import, which put the raw frontmatter name into the ID before opening the editor in clone mode, where the reactive slugify is disabled.
Existing rows with an unreachable ID are not repaired here; rewriting a primary key would also have to re-point the access grants keyed on it.
Fixes #27655
This commit is contained in:
parent
954613944b
commit
3df485582d
3 changed files with 11 additions and 3 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
|
|
@ -180,6 +181,13 @@ async def create_new_skill(
|
|||
|
||||
form_data.id = form_data.id.lower().replace(' ', '-')
|
||||
|
||||
# The id goes into /id/{id}/... paths, so anything outside the slug charset is unreachable once stored.
|
||||
if not re.fullmatch(r'[a-z0-9_-]+', form_data.id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT('Invalid skill ID'),
|
||||
)
|
||||
|
||||
existing = await Skills.get_skill_by_id(form_data.id, db=db)
|
||||
if existing is not None:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
deleteSkillById,
|
||||
toggleSkillById
|
||||
} from '$lib/apis/skills';
|
||||
import { capitalizeFirstLetter, parseFrontmatter, formatSkillName } from '$lib/utils';
|
||||
import { capitalizeFirstLetter, parseFrontmatter, formatSkillName, slugify } from '$lib/utils';
|
||||
import TagInput from '$lib/components/common/Tags/TagInput.svelte';
|
||||
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
|
|
@ -306,7 +306,7 @@
|
|||
const displayName = formatSkillName(rawName);
|
||||
sessionStorage.skill = JSON.stringify({
|
||||
name: displayName,
|
||||
id: fm.name || '',
|
||||
id: slugify(rawName),
|
||||
description: fm.description || '',
|
||||
content: mdContent,
|
||||
is_active: true,
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@
|
|||
const fm = parseFrontmatter(content);
|
||||
if (fm.name && !name) {
|
||||
name = formatSkillName(fm.name);
|
||||
id = fm.name;
|
||||
}
|
||||
if (fm.description && !description) {
|
||||
description = fm.description;
|
||||
|
|
@ -52,6 +51,7 @@
|
|||
return;
|
||||
}
|
||||
loading = true;
|
||||
if (!edit) id = slugify(id);
|
||||
|
||||
await onSubmit({
|
||||
id,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue