collectionPaths() as $path) { foreach ($this->hostileQueries() as $query) { $response = $this->requestAs($this->actorFor($path), 'GET', $path.'?'.http_build_query($query)); $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "hostile query $path"); $this->assertNoServerError($response, "hostile query $path"); } } } public function test_date_range_endpoints_reject_inverted_or_impossible_ranges_cleanly(): void { foreach ($this->dateRangePaths() as $path) { foreach ($this->badDateRanges() as $query) { $response = $this->requestAs($this->actorFor($path), 'GET', $path.'?'.http_build_query($query)); $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "bad date range $path"); $this->assertNoServerError($response, "bad date range $path"); } } } public function test_sorting_contract_does_not_allow_raw_sql_fragments(): void { foreach ($this->collectionPaths() as $path) { $response = $this->requestAs($this->actorFor($path), 'GET', $path.'?'.http_build_query([ 'sort' => 'id desc; drop table users; --', 'direction' => 'sideways', ])); $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "SQL-ish sort $path"); $this->assertNoServerError($response, "SQL-ish sort $path"); $this->assertStringNotContainsString('SQLSTATE', $response->getContent(), "$path leaked SQLSTATE for malicious sort."); } } /** @return list */ private function collectionPaths(): array { return [ 'api/v1/users', 'api/v1/students', 'api/v1/classes', 'api/v1/attendance', 'api/v1/finance/invoices', 'api/v1/finance/payments', 'api/v1/inventory/items', 'api/v1/messages', 'api/v1/support', 'api/v1/report-cards', ]; } /** @return list> */ private function hostileQueries(): array { return [ ['page' => -1, 'per_page' => 0], ['page' => 'abc', 'per_page' => '999999999999'], ['search' => str_repeat('x', 5000)], ['status' => ['nested' => ['bad' => true]]], ['include' => '../../../.env'], ]; } /** @return list */ private function dateRangePaths(): array { return [ 'api/v1/attendance', 'api/v1/finance/reports', 'api/v1/finance/payments', 'api/v1/report-cards', 'api/v1/messages', ]; } /** @return list> */ private function badDateRanges(): array { return [ ['start_date' => '2026-12-31', 'end_date' => '2025-01-01'], ['from' => 'not-a-date', 'to' => 'also-not-a-date'], ['date' => '0000-00-00'], ['school_year' => '../../2025', 'semester' => ''], ]; } }