[Detail Bug] Catalog API: GET /hardware allows unbounded results via negative `limit`
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
Research direction
Start in packages/catalog/src/xknxmono/catalog/core/hardware.py, reading HardwareFilters and the query path through CatalogService.list_hardware and list_hardware; then inspect the GET /hardware router binding. Done means out-of-range limit and offset inputs are rejected or bounded before SQLAlchemy receives them, while valid pagination continues to work.
Written by the indexing model from the issue text.
Description
Detail Bug Report
Introduced in 34216ee503c1c3cc4b43216687fc65b399d1031f by @kewde on Jun 2, 2026
Summary
- Context: The
GET /hardwareendpoint inpackages/catalog/src/xknxmono/catalog/http/routers/hardware.pyacceptslimitandoffsetas query parameters via theHardwareFiltersPydantic model (defined inpackages/catalog/src/xknxmono/catalog/core/hardware.py). These values flow unchanged throughCatalogService.list_hardware→list_hardware→ SQLAlchemy's.offset(filters.offset).limit(filters.limit). - Bug:
HardwareFilters.limithas no validation constraint — any integer, including negative or arbitrarily large values, is accepted and passed to SQLAlchemy's.limit(). A client sending?limit=-1causes SQLAlchemy to emitLIMIT -1, which SQLite interprets as no limit (return all rows). The endpoint is unauthenticated (no auth dependency, no rate limiting; the only middleware isCORSMiddleware(allow_origins=["*"], allow_methods=["*"])). NoField(le=...),field_validator, or router-levelQuery(le=...)clamps the value anywhere. Thelimitfield's own docstring states "Maximum number of results to return (hard cap enforced by the HTTP layer)" — but no such cap has ever been implemented on the model, and no cap exists at the HTTP layer at HEAD. - Actual vs. expected:
?limit=-1returns every row in thehardwaretable with no upper bound. Expected behavior: an input-validatedlimitthat rejects or clamps out-of-range values, as the docstring describes. - Impact: The harm is per-request peak memory/CPU during ORM hydration + Pydantic serialization and unbounded response bytes to a client. This is not a data-access or confidentiality boundary: a paginating client (
?limit=200&offset=0,200,…) can already enumerate the entire table. What the missing cap adds is the ability to force full-table serialization in a single request without a pagination loop.
Code with Bug
# packages/catalog/src/xknxmono/catalog/core/hardware.py
class HardwareFilters(BaseModel):
...
limit: int = 50 # <-- BUG 🔴 no bounds; negative becomes "unlimited" on SQLite
"""Maximum number of results to return (hard cap enforced by the HTTP layer)."""
offset: int = 0
"""Number of results to skip before returning (for pagination)."""
# packages/catalog/src/xknxmono/catalog/core/hardware.py
q = q.distinct().offset(filters.offset).limit(filters.limit) # <-- BUG 🔴 passes unvalidated limit into SQL
Explanation
HardwareFiltersis used directly as the FastAPI query-parameter schema (Annotated[HardwareFilters, Query()]), butlimithas noField(le=...)/validator. As a result,GET /hardware?limit=-1reaches SQLAlchemy as.limit(-1).- On SQLite,
LIMIT -1is documented/observed to mean “no limit”, so the query returns all rows and the server hydrates/serializes the entire table into one response.
Recommended Fix
# packages/catalog/src/xknxmono/catalog/core/hardware.py
from pydantic import Field
limit: int = Field(default=50, ge=0, le=200)
"""Maximum number of results to return (hard cap enforced by the HTTP layer)."""
offset: int = Field(default=0, ge=0)
"""Number of results to skip before returning (for pagination)."""
History
This bug was introduced in commit 34216ee. The commit ("feat(catalog): add CatalogService and route the HTTP API through it") converted HardwareFilters from a stdlib @dataclass into a Pydantic BaseModel and switched the router from Annotated[HardwareFilters, Depends()] to Annotated[HardwareFilters, Query()], making the model the live FastAPI query-parameter schema. The cap (limit: int = Query(50, le=200)) had already been deleted in the earlier 2d0b96b refactor, but until 34216ee the Depends()+dataclass form left a default_factory sentinel that broke /hardware query binding entirely (the commit message itself notes this), so the uncapped limit never reached the ORM. By unifying the model with the HTTP binding surface as a Pydantic model without adding Field(le=200), 34216ee is the first commit where the endpoint works AND an out-of-range ?limit=-1 flows unchecked into SQLAlchemy's .limit(-1) — i.e. the first end-to-end exploitable state.
- Dominant language
- Python
- Stars
- 4
- Forks
- 0
- Avg merge
- 15h 38m
- Merged PRs (30d)
- 37
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from XKNX/xknxtoolkit
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
XKNX/xknxtoolkit#109 ·
-
[Detail Bug] Dynamic UI evaluation crashes or mangles labels when TextArg values contain backslashes Open
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
XKNX/xknxtoolkit#100 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
XKNX/xknxtoolkit#98 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
XKNX/xknxtoolkit#97 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
XKNX/xknxtoolkit#94 ·
All issues in XKNX/xknxtoolkit
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
bancolombia/sentinel#23 ·
-
test md OpenCI
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
integration:quickjs org:external priority:backlog topic:code-interpreter topic:middleware type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
langchain-ai/deepagents#6450 ·
-
bug client
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100