fix: escape SQL wildcards in tag search autocomplete

The % and _ characters in user search input are now escaped before
the LIKE query, preventing unintended wildcard matching in tag search.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-06 00:26:35 -05:00
parent 94ec19cf07
commit 1e57aa8323
2 changed files with 33 additions and 1 deletions

View File

@@ -62,8 +62,9 @@ async def search_tags(
Searches tag names for the query string. Searches tag names for the query string.
Returns matching tags ordered by usage count. Returns matching tags ordered by usage count.
""" """
escaped_q = q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
query = select(TreeTag).where( query = select(TreeTag).where(
TreeTag.name.ilike(f"%{q}%") TreeTag.name.ilike(f"%{escaped_q}%", escape="\\")
) )
# Filter by visibility # Filter by visibility

View File

@@ -275,6 +275,37 @@ class TestTrees:
) )
assert len(tag_rows_after.fetchall()) == 0 assert len(tag_rows_after.fetchall()) == 0
@pytest.mark.asyncio
async def test_tag_search_escapes_wildcards(
self, client: AsyncClient, admin_auth_headers: dict
):
"""Test that SQL wildcards in tag search are escaped, not interpreted."""
# Create tags as admin (can create global tags)
resp1 = await client.post(
"/api/v1/tags",
json={"name": "test_underscore"},
headers=admin_auth_headers
)
assert resp1.status_code == 201
resp2 = await client.post(
"/api/v1/tags",
json={"name": "testXunderscore"},
headers=admin_auth_headers
)
assert resp2.status_code == 201
# Search for literal underscore — should only match the first tag
response = await client.get(
"/api/v1/tags/search",
params={"q": "test_under"},
headers=admin_auth_headers
)
assert response.status_code == 200
names = [t["name"] for t in response.json()]
assert "test_underscore" in names
assert "testXunderscore" not in names
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_tree_unauthorized(self, client: AsyncClient): async def test_create_tree_unauthorized(self, client: AsyncClient):
"""Test that creating a tree without auth fails.""" """Test that creating a tree without auth fails."""