|
|
@@ -144,6 +144,49 @@ final class AuditLogControllerTest extends AppTestCase
|
|
|
self::assertArrayHasKey('subject', $this->decode($resp)['details']);
|
|
|
}
|
|
|
|
|
|
+ public function testKindFilterCharsetGate(): void
|
|
|
+ {
|
|
|
+ // SEC_REVIEW F47: `entity_type` and `subject_kind` are matched
|
|
|
+ // against `actor_kind` / `target_type` columns whose real values
|
|
|
+ // are short snake/kebab identifiers. Reject anything outside
|
|
|
+ // `^[a-z0-9][a-z0-9_-]*$` so wild bytes can't reach the
|
|
|
+ // prepared statement (no SQLi today, but the zero-byte and
|
|
|
+ // multi-megabyte cases waste planner work).
|
|
|
+ $token = $this->createToken(TokenKind::Admin, Role::Viewer);
|
|
|
+
|
|
|
+ // entity_type with uppercase / dot / space
|
|
|
+ foreach (['Reporter', 'reporter.created', 'reporter manual', '-leading-dash', "report\nfaked"] as $bad) {
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?entity_type=' . rawurlencode($bad),
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(400, $resp->getStatusCode(), "expected 400 for entity_type={$bad}");
|
|
|
+ self::assertArrayHasKey('entity_type', $this->decode($resp)['details']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // subject_kind needs the same gate (paired with subject_id).
|
|
|
+ foreach (['Reporter', 'reporter.created', 'rep orter', '-leading-dash'] as $bad) {
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?subject_kind=' . rawurlencode($bad) . '&subject_id=5',
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(400, $resp->getStatusCode(), "expected 400 for subject_kind={$bad}");
|
|
|
+ self::assertArrayHasKey('subject', $this->decode($resp)['details']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Real kinds still pass through (smoke).
|
|
|
+ foreach (['reporter', 'consumer', 'manual_block', 'admin-token', 'oidc_role_mapping'] as $good) {
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?entity_type=' . rawurlencode($good),
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(200, $resp->getStatusCode(), "expected 200 for entity_type={$good}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
public function testDeepOffsetRejected(): void
|
|
|
{
|
|
|
// SEC_REVIEW F31: pagination is capped at 10 000 offset rows so a
|