Repository navigation
Bound client input at the API boundary - #82
Merged
Merged
Conversation
- POST /api/frontend-error: typed FrontendErrorReport (message, stack,
component, url) with max_length bounds; the body is read as a stream and
rejected with 413 above 128 KiB, invalid bodies get 422 instead of a 500,
and each logged field is truncated to 4000 chars
- WebSocket (player and spectator): messages above 64 KiB (UTF-8 bytes) get
a localised error reply and the loop keeps running
- parse_action: params that are not an object (or have non-string keys),
carry more than 16 entries or a string over 2000 chars are rejected with
a localised protocol error instead of being silently replaced by {}
- UpdateLayerFileRequest.content capped at 1 MiB
- GiveItemRequest: 5e-sensible numeric ranges (price, reach, base_ac,
max_dex_bonus, strength_req, ac_bonus) and string/list size caps
- SpawnCreatureRequest accepts xp_value (>= 0); spawned monsters and NPCs
award it on kill
Owner
Author
|
Independent review: PASS
|
vladmesh
pushed a commit
that referenced
this pull request
Sep 29, 2026
Resolve routes_ws.py: keep #82's message-size and params bounds and send the params errors through _send_session_error so they use the session language (the size error is already rendered under the session language by the envelope parse). Keep both sides' ru translations, regenerate messages.pot, recompile the .mo, and extend the ru-session WS test with the three new errors.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Card W3-1: API boundary hardening (issue 4f6495b0f969e865d779 remainder + cfba11ddde265d501076 spawn-api-xp-value). Based on main 2f79530. None of the six items was already fixed on main.
Root cause / fix per item
POST /api/frontend-error: it readawait request.json()with no schema or size limit and loggedmessage/stackwithout bounds.FrontendErrorReportmodel (message,stack,component,url) withmax_lengthbounds.main.tsxsends{message, stack}andErrorBoundaryaddscomponent;urlis optional for later use.[truncated N chars]marker.componentis now logged too (before, it was dropped).openapi_extra. The frontend reporters are unchanged and still get 200.WebSocket message size (player and spectator loops):
_parse_json_object_enveloperejects any message overMAX_WS_MESSAGE_BYTES = 64 KiB, measured in UTF-8 bytes, beforejson.loads. The client gets a localised{"type":"error"}reply and the loop keeps running. The largest legitimate client message is an action envelope withsaytext, which is far below this limit.UpdateLayerFileRequest.content: capped at 1 MiB. The largest shipped layer file islibrary/geography/sword_vale/locations.yamlat about 18 KiB.GiveItemRequestbounds:price0..1,000,000 gpreach5..30 ftbase_ac10..30 (SRD armour runs 11..18)max_dex_bonus0..10strength_req0..30ac_bonus0..10damageis capped at 8 entries andgrant_actionsat 16.GiveItemDialogpayload (reach defaults to 5) stays inside these ranges.Action params: typed param values were already checked on main (
rules/action_params.validate_action_paramscovers string/integer types and required params). Three gaps remained inservice/action_parsing.parse_action:params("x",[..],1) were silently replaced with{}. For the dispatcher, an empty dict means "probe", so a malformed action was turned into a different request.saytext of any length went into the event log, saves and LLM prompts.Now:
params: nullor a missingparamsstill means{}. Anything else that is not an object with string keys raisesActionParamsError("not_object"). More than 16 entries, or a string value over 2000 chars, raisesActionParamsError("too_large"). The WS adapter answers with a localised protocol error and the loop continues.SpawnCreatureRequest.xp_value: added asint | None = Field(default=None, ge=0), the same bound as in PATCH.xp_valuethroughNpcContent, but pydantic dropped the field from the request._parse_spawnnow passes it toCreature(xp_value=...).i18n: three new player-facing strings, added to
messages.potand the ru.po, and the.morecompiled. I added the.potentries by hand because runningmake messageson main produces about 470 lines of unrelated churn: the.potis stale on main. No frontend i18n was needed, since the WS error text is rendered on the server.Tests
New file
tests/unit/test_api_boundary_bounds.pywith 43 cases, all going through the real FastAPI app or WS unless noted:{"message": 5}, a non-string stack,[], non-JSON) get 422parse_actionunit cases: non-object params, non-string keys, too many entries, over-long string, and the accepted boundary values.body.contentbefore the service is called.GiveItemRequest: 12 out-of-range cases get 422 and the inventory is unchanged; an in-range reach weapon is accepted.xp_value: for both monster and NPC, spawning through the API and then killing throughcombat_resolution.handle_deathgrants exactlyxp_valueand emitsXP_GAINED. A negativexp_valuegets 422 and nothing is spawned.Checked against main: I ran the same test file against main's
src/(with the new constants inlined), and 40 of 43 fail. The 3 that pass on both are the happy-path guards: reporter payloads logged, bounded params accepted, in-range weapon given.Checks:
make checkis green: ruff, mypy (185 files), 2978 backend unit tests, and frontend eslint/tsc/vitest (353 tests).tests/integration/against a local uvicorn backend with thedocker-compose.test.ymlsettings (DND_DICE_SEED=42,DND_CONTENT_DIR=tests/integration/content,BACKEND_URL=http://localhost:8001). All 184 passed.Real bug found by the new tests (fixed here)
POST /api/frontend-errorwith a JSON body that is not an object (for example[]or"x"), or with a non-JSON body, returned 500 from an unhandled exception instead of a client error.body.get(...)on whateverrequest.json()returned and had no validation.ValidationErrormaps to 422 through the existing app handler).TestFrontendErrorReport::test_invalid_body_is_422[[]]/[not json].A second one:
xp_valuesent on NPC spawn was silently ignored.test_spawned_creature_awards_its_xp_value_on_kill[npc].Found but not fixed (out of scope)
saytext inputs (Perception.tsx,NpcInspectModal.tsx) have nomaxLength. The server now rejects text over 2000 chars with an error; the UI could addmaxLength={2000}to prevent it up front.messages.potis stale on main: regenerating it adds or reorders about 30 msgids unrelated to this change.PUT /api/master/worlds/{id}/entities/...andPUT /api/master/catalogs/...takebody: dict[str, Any]with no size bound. Pydantic content models validate the shape afterwards.SpawnCreatureRequesthas no length bounds onid/name/personality, and itsattacks: list[dict]is unbounded.SpawnCreatureRequestTS type does not includexp_value; no GM UI sends it yet.