[Detail Bug] Catalog API: GET /hardware allows unbounded results via negative `limit`

Open Beginner friendly
#104 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
fastapi, python, sqlalchemy, sqlite
Domain
api, backend, databases

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

https://app.detail.dev/org_62aa40f5-2c23-4914-a665-3bb2068af20e/bugs/bug_923e257b-563b-4231-bb3f-305733742ac5

Introduced in 34216ee503c1c3cc4b43216687fc65b399d1031f by @kewde on Jun 2, 2026

Summary

  • Context: The GET /hardware endpoint in packages/catalog/src/xknxmono/catalog/http/routers/hardware.py accepts limit and offset as query parameters via the HardwareFilters Pydantic model (defined in packages/catalog/src/xknxmono/catalog/core/hardware.py). These values flow unchanged through CatalogService.list_hardwarelist_hardware → SQLAlchemy's .offset(filters.offset).limit(filters.limit).
  • Bug: HardwareFilters.limit has no validation constraint — any integer, including negative or arbitrarily large values, is accepted and passed to SQLAlchemy's .limit(). A client sending ?limit=-1 causes SQLAlchemy to emit LIMIT -1, which SQLite interprets as no limit (return all rows). The endpoint is unauthenticated (no auth dependency, no rate limiting; the only middleware is CORSMiddleware(allow_origins=["*"], allow_methods=["*"])). No Field(le=...), field_validator, or router-level Query(le=...) clamps the value anywhere. The limit field'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=-1 returns every row in the hardware table with no upper bound. Expected behavior: an input-validated limit that 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

  • HardwareFilters is used directly as the FastAPI query-parameter schema (Annotated[HardwareFilters, Query()]), but limit has no Field(le=...)/validator. As a result, GET /hardware?limit=-1 reaches SQLAlchemy as .limit(-1).
  • On SQLite, LIMIT -1 is 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from XKNX/xknxtoolkit

All issues in XKNX/xknxtoolkit

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.