99 lines
3.5 KiB
PHP
99 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Api\V1\FullSurface;
|
|
|
|
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
|
|
|
class ApiQueryParameterFuzzingContractTest extends FullSurfaceE2EContractCase
|
|
{
|
|
public function test_collection_endpoints_handle_hostile_query_parameters_without_5xx(): void
|
|
{
|
|
foreach ($this->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<string> */
|
|
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<array<string,mixed>> */
|
|
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<string> */
|
|
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<array<string,string>> */
|
|
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' => '<script>alert(1)</script>'],
|
|
];
|
|
}
|
|
}
|