Ticket create 500s when group_id is submitted as a JSON array #10

Closed
opened 2026-07-28 13:04:49 -07:00 by jwed · 1 comment
Owner

Summary

POST /app/ticketing (the ticket-create endpoint, ticketing.store) throws an
unhandled 500 when group_id is submitted as a JSON array instead of a scalar
integer, instead of returning a normal 422 validation error.

Steps to reproduce

Logged in as superadmin@playwright.test. From a browser session on
https://central-dev.vancs.org/app/ticketing/create (so cookies/XSRF token are
valid), issue:

fetch('/app/ticketing', {
  method: 'POST',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'X-XSRF-TOKEN': /* value of XSRF-TOKEN cookie */,
  },
  body: JSON.stringify({ title: 't', description: 'x', group_id: [14] }),
});

(14 is the id of the real "General" ticketing group in this dev environment —
substitute any id that actually exists.)

Expected

A 422 with a validation error on group_id (e.g. "The group id field must be
an integer."), the same way group_id: ['a','b'] (an array of values that
don't exist) correctly returns:

{"message":"The selected group id is invalid.","errors":{"group_id":["The selected group id is invalid."]}}

Actual

A 500 with no further detail leaked (APP_DEBUG appears to be off — response
body is just {"message":"Server Error"}, no stack trace):

{
    "message": "Server Error"
}

Analysis

Laravel's exists validation rule, when given an array value for the field,
validates that every element of the array exists in the table — this is
normally intended for multi-select fields. Since 14 really is a valid group
id, group_id: [14] passes the exists check as an array of one valid id,
so validation lets it through. The controller then presumably treats
$request->group_id as a scalar (e.g. passes it straight into a query or an
int column), and something downstream (an array where an int/scalar was
expected) throws, producing the 500.

A value made entirely of non-existent entries (['a','b']) is correctly
rejected by the exists check itself before reaching that code, which is why
only some array shapes reproduce this specifically.

Suggested fix

Add an explicit integer (or sometimes|integer) rule ahead of exists on
group_id in the ticket-store request validation, so a non-scalar value is
rejected with a normal 422 before ever reaching the exists check or the
controller.

Environment

  • URL: https://central-dev.vancs.org/app/ticketing (POST)
  • Persona: superadmin@playwright.test
  • Route name: ticketing.store (confirmed via route().current() → ticketing.index prefix ticketing)
  • No stack trace or internal path was leaked in the response — this is a
    robustness/crash bug, not an information-disclosure bug.

Filed automatically by the Claude-driven QA sweep (/opt/qa-agent). The marker below dedupes repeat reports — do not edit it.

## Summary `POST /app/ticketing` (the ticket-create endpoint, `ticketing.store`) throws an unhandled 500 when `group_id` is submitted as a JSON array instead of a scalar integer, instead of returning a normal 422 validation error. ## Steps to reproduce Logged in as `superadmin@playwright.test`. From a browser session on `https://central-dev.vancs.org/app/ticketing/create` (so cookies/XSRF token are valid), issue: ```js fetch('/app/ticketing', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-XSRF-TOKEN': /* value of XSRF-TOKEN cookie */, }, body: JSON.stringify({ title: 't', description: 'x', group_id: [14] }), }); ``` (`14` is the id of the real "General" ticketing group in this dev environment — substitute any id that actually exists.) ## Expected A 422 with a validation error on `group_id` (e.g. "The group id field must be an integer."), the same way `group_id: ['a','b']` (an array of values that don't exist) correctly returns: ```json {"message":"The selected group id is invalid.","errors":{"group_id":["The selected group id is invalid."]}} ``` ## Actual A 500 with no further detail leaked (APP_DEBUG appears to be off — response body is just `{"message":"Server Error"}`, no stack trace): ```json { "message": "Server Error" } ``` ## Analysis Laravel's `exists` validation rule, when given an array value for the field, validates that *every element of the array* exists in the table — this is normally intended for multi-select fields. Since `14` really is a valid group id, `group_id: [14]` passes the `exists` check as an array of one valid id, so validation lets it through. The controller then presumably treats `$request->group_id` as a scalar (e.g. passes it straight into a query or an `int` column), and something downstream (an array where an int/scalar was expected) throws, producing the 500. A value made entirely of non-existent entries (`['a','b']`) is correctly rejected by the `exists` check itself before reaching that code, which is why only some array shapes reproduce this specifically. ## Suggested fix Add an explicit `integer` (or `sometimes|integer`) rule ahead of `exists` on `group_id` in the ticket-store request validation, so a non-scalar value is rejected with a normal 422 before ever reaching the `exists` check or the controller. ## Environment - URL: `https://central-dev.vancs.org/app/ticketing` (POST) - Persona: `superadmin@playwright.test` - Route name: `ticketing.store` (confirmed via `route().current()` → `ticketing.index` prefix `ticketing`) - No stack trace or internal path was leaked in the response — this is a robustness/crash bug, not an information-disclosure bug. --- _Filed automatically by the Claude-driven QA sweep (`/opt/qa-agent`). The marker below dedupes repeat reports — do not edit it._ <!-- claude-qa-fingerprint: 22f0a3d9324d -->
jwed added the security label 2026-07-28 13:04:49 -07:00
Author
Owner

Fixed in afbbd3f.

Your analysis was right: exists given an array validates that every element exists, so group_id: [14] passed with one valid id and reached the controller as an array — findOrFail() returned a Collection and ->nextTicketNumber() on it fatalled. ["a","b"] was rejected by exists itself, which is why only arrays of valid ids reproduced it.

Added integer ahead of exists as you suggested, but the hole is a property of exists rather than of group_id, so all nine id fields across both ticketing controllers had it — store() group_id/priority_id, update() priority_id/project_id/group_id, and storeAgent/storeProject/storeEmailConnection group_id plus user_id (which fed straight into an access check).

Tests pin the repro, the same thing on priority_id, that a nonexistent id still 422s, and that a normal scalar create still works. Confirmed the array test fails with the rules reverted.

Fixed in afbbd3f. Your analysis was right: `exists` given an array validates that every element exists, so `group_id: [14]` passed with one valid id and reached the controller as an array — `findOrFail()` returned a Collection and `->nextTicketNumber()` on it fatalled. `["a","b"]` was rejected by `exists` itself, which is why only arrays of *valid* ids reproduced it. Added `integer` ahead of `exists` as you suggested, but the hole is a property of `exists` rather than of group_id, so all nine id fields across both ticketing controllers had it — `store()` group_id/priority_id, `update()` priority_id/project_id/group_id, and storeAgent/storeProject/storeEmailConnection group_id plus user_id (which fed straight into an access check). Tests pin the repro, the same thing on priority_id, that a nonexistent id still 422s, and that a normal scalar create still works. Confirmed the array test fails with the rules reverted.
jwed closed this issue 2026-07-28 16:59:31 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jwed/central-ticketing#10