Skip to content

Out-of-range version path parameter 500s instead of 404 on evaluator and importer version routes #928

Description

@strickvl

Found by the Schemathesis API fuzzing spike (#923). Verified by hand against the real app on real PostgreSQL.

What happens

The plugin-version routes declare their version path parameter as a bare int, so FastAPI accepts any Python integer. PostgreSQL's plugin_version.version column is an INTEGER (32-bit), and asyncpg refuses to encode anything outside -2^31 .. 2^31-1. The result is a 500 where a 404 belongs.

GET /api/v1/evaluators/41813831-a5f0-4833-8447-4a69fbff83e9/versions/681812836839
Authorization: Bearer <token>

500 Internal Server Error.

Captured exception:

sqlalchemy.exc.DBAPIError: (sqlalchemy.dialects.postgresql.asyncpg.Error)
  <class 'asyncpg.exceptions.DataError'>: invalid input for query argument $2:
  681812836839 (value out of int32 range)
[SQL: SELECT plugin_version.created, ... FROM plugin_version
      WHERE plugin_version.plugin_id = $1::UUID AND plugin_version.version = $2::INTEGER]

Control: /versions/7404 with a proper JSON detail, which is what an out-of-range version should also produce. Negative overflow (/versions/-3000000000) fails identically.

Affected operations

Four, all with the same shape:

  • GET /api/v1/evaluators/{evaluator_id}/versions/{version}src/kitaru/server/adapters/rest/routers/evaluators.py:220
  • PATCH /api/v1/evaluators/{evaluator_id}/versions/{version}evaluators.py:245
  • GET /api/v1/importers/{importer_id}/versions/{version}src/kitaru/server/adapters/rest/routers/importers.py:220
  • PATCH /api/v1/importers/{importer_id}/versions/{version}importers.py:246

Path through the code

The handler passes the raw int through PluginService into PluginRepository.get_version (src/kitaru/server/adapters/db/repositories/plugin_repository.py:277-279), which binds it against the INTEGER column. asyncpg raises DataError, SQLAlchemy wraps it as DBAPIError, and the handler at src/kitaru/server/api/app.py:175-189 only converts deadlocks and lost connections — everything else it re-raises, so the request 500s.

Impact

Low. It needs a valid bearer token under the LOCAL auth scheme, so it is an authenticated-user crash vector rather than an anonymous one. One handled 500 per request, no process crash, no data exposure, no resource exhaustion. Safe to file and fix publicly.

Suggested fix

Constrain the parameter at the API boundary in all four handlers:

version: Annotated[int, Path(ge=1, le=2_147_483_647)]

FastAPI then rejects out-of-range values with a 422 before any database call, and the bounds land in openapi/openapi.json automatically — which also closes the secondary schema gap, since the spec currently declares a bare type: integer with no bounds and so tells clients the wrong contract. Regenerate with scripts/generate_openapi.py and rerun pnpm run generate for the TypeScript types.

If a 404 is preferred over a 422 for symmetry with "version not found", clamp in the service instead — but the Path constraint is smaller and self-documenting.

Regression test

Assert 422 (not 500) for 2**31 and for -2**31 - 1 on one of the four routes. Needs PostgreSQL, since SQLite would accept the value.

Related

Shares a root cause with the NUL-byte class: the database's own constraints are the only validator, and its complaints arrive as server errors rather than client errors. The backstop proposed there — teaching the DBAPIError handler to recognize asyncpg's DataError family and answer 422 — would also catch this one.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions