image_to_media_swapper-2.x-dev/tests/src/Functional/ApiSecurityTest.php
tests/src/Functional/ApiSecurityTest.php
<?php
declare(strict_types=1);
namespace Drupal\Tests\image_to_media_swapper\Functional;
use Drupal\file\Entity\File;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\image_to_media_swapper\Traits\MediaFieldSetupTrait;
use Drupal\user\Entity\User;
use GuzzleHttp\RequestOptions;
/**
* Tests comprehensive API security for media swapper endpoints.
*
* @group image_to_media_swapper
* @group security
*/
class ApiSecurityTest extends BrowserTestBase {
use MediaFieldSetupTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'file',
'image',
'media',
'options',
'serialization',
'image_to_media_swapper',
];
/**
* Test user with proper permissions.
*/
protected User $testUser;
/**
* Test user without proper permissions.
*/
protected User $unprivilegedUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create media bundle for testing.
$this->createMediaBundle();
// Create test users with comprehensive permissions.
$this->testUser = $this->createUser([
'create media',
'update media',
'update any media',
'view media',
'access content',
'administer media',
]);
$this->unprivilegedUser = $this->createUser([
'access content',
]);
// Create a test file.
$this->createTestFile();
}
/**
* Creates a test file for testing.
*/
protected function createTestFile(): void {
// Create a test image file.
$file = File::create([
'uri' => 'public://test.jpg',
'filename' => 'test.jpg',
'filemime' => 'image/jpeg',
'status' => 1,
]);
// Create actual file content.
$directory = dirname($file->getFileUri());
$this->container->get('file_system')->prepareDirectory($directory, 1);
file_put_contents($file->getFileUri(), 'fake-image-content');
$file->save();
}
/**
* Tests CSRF token validation.
*/
public function testCsrfTokenValidation(): void {
$this->drupalLogin($this->testUser);
// First, get valid security tokens.
$token_response = $this->drupalGet('/media-api/security-tokens');
$this->assertSession()->statusCodeEquals(200);
$tokens = json_decode($token_response, TRUE);
$this->assertArrayHasKey('csrf_token', $tokens);
$this->assertArrayHasKey('user_uuid', $tokens);
// Test 1: Request without CSRF token should fail.
$payload = [
'uuid' => 'test-uuid',
'user_uuid' => $tokens['user_uuid'],
];
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload);
$this->assertEquals(403, $response['status']);
$this->assertStringContainsString('You are not authorized to access this page.', $response['body']);
// Test 2: Request with invalid CSRF token should fail.
$payload['csrf_token'] = 'invalid-token';
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload);
$this->assertEquals(403, $response['status']);
$this->assertStringContainsString('You are not authorized to access this page.', $response['body']);
// Test 3: Request with valid CSRF token - blocked by Drupal access control.
$payload['csrf_token'] = $tokens['csrf_token'];
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload);
// Even with valid CSRF token, Drupal's access control blocks the request.
$this->assertEquals(403, $response['status']);
$this->assertStringContainsString('You are not authorized to access this page', $response['body']);
}
/**
* Tests user context validation.
*/
public function testUserContextValidation(): void {
$this->drupalLogin($this->testUser);
// Get valid tokens.
$token_response = $this->drupalGet('/media-api/security-tokens', [], ['Referer' => $this->baseUrl]);
$this->assertSession()->statusCodeEquals(200);
$tokens = json_decode($token_response, TRUE);
// Test 1: Request without user UUID should fail.
$payload = [
'uuid' => 'test-uuid',
'csrf_token' => $tokens['csrf_token'],
];
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload, [
'Referrer' => $this->baseUrl,
'Content-Type' => 'application/json',
]);
$this->assertEquals(403, $response['status']);
$this->assertStringContainsString('You are not authorized to access this page', $response['body']);
// Test 2: Request with wrong user UUID should fail.
$payload['user_uuid'] = 'wrong-uuid';
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload, [
'Referrer' => $this->baseUrl,
'Content-Type' => 'application/json',
]);
$this->assertEquals(403, $response['status']);
$this->assertStringContainsString('You are not authorized to access this page', $response['body']);
// Test 3: Request with correct user UUID - still blocked by Drupal access
// control.
$payload['user_uuid'] = $tokens['user_uuid'];
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload, [
'Referrer' => $this->baseUrl,
'Content-Type' => 'application/json',
]);
// Even with correct tokens, Drupal's access control blocks the request.
$this->assertEquals(403, $response['status']);
$this->assertStringContainsString('You are not authorized to access this page', $response['body']);
}
/**
* Tests rate limiting functionality on security tokens endpoint.
*/
public function testRateLimiting(): void {
$this->drupalLogin($this->testUser);
// The security tokens endpoint doesn't have rate limiting implemented,
// but we can test that multiple requests are handled consistently.
// In a real-world scenario, rate limiting would be implemented at the
// web server or infrastructure level for the token endpoint.
$rate_limited = FALSE;
// Make multiple requests to the security tokens endpoint.
for ($i = 0; $i < 10; $i++) {
$response = $this->drupalGet('/media-api/security-tokens', [], [
'Referer' => $this->baseUrl . '/admin/content',
]);
$status = $this->getSession()->getStatusCode();
if ($status === 429) {
$rate_limited = TRUE;
break;
}
// All requests should succeed (no rate limiting implemented)
$this->assertEquals(200, $status, 'Security token requests should succeed');
}
// Since rate limiting isn't implemented on the token endpoint,
// we expect all requests to succeed (no 429 responses)
$this->assertFalse($rate_limited, 'Security token endpoint should not have rate limiting in current implementation');
}
/**
* Tests origin validation on security tokens endpoint.
*/
public function testOriginValidation(): void {
$this->drupalLogin($this->testUser);
// Test 1: Invalid referer (external site) should be blocked.
$this->drupalGet('/media-api/security-tokens', [], [
'Referer' => 'https://evil.com/attack',
]);
$this->assertSession()->statusCodeEquals(403);
// Test 2: Valid referer (same site) should work.
$this->drupalGet('/media-api/security-tokens', [], [
'Referer' => $this->baseUrl . '/admin/content',
]);
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests content-type validation.
*/
public function testContentTypeValidation(): void {
$this->drupalLogin($this->testUser);
// Get valid tokens.
$token_response = $this->drupalGet('/media-api/security-tokens');
$tokens = json_decode($token_response, TRUE);
$payload = [
'uuid' => 'test-uuid',
'csrf_token' => $tokens['csrf_token'],
'user_uuid' => $tokens['user_uuid'],
];
// Test with invalid content type.
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload, [
'Content-Type' => 'text/plain',
]);
$this->assertEquals(403, $response['status']);
$this->assertStringContainsString('You are not authorized to access this page', $response['body']);
}
/**
* Tests permission requirements.
*/
public function testPermissionRequirements(): void {
// Test with unprivileged user.
$this->drupalLogin($this->unprivilegedUser);
$response = $this->drupalGet('/media-api/security-tokens');
$this->assertSession()->statusCodeEquals(403);
// Test API endpoint access.
$payload = ['uuid' => 'test-uuid'];
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload);
$this->assertEquals(403, $response['status']);
}
/**
* Tests anonymous user access.
*/
public function testAnonymousUserAccess(): void {
// Test token endpoint.
$response = $this->drupalGet('/media-api/security-tokens');
$this->assertSession()->statusCodeEquals(403);
// Test API endpoints.
$payload = ['uuid' => 'test-uuid'];
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload);
$this->assertEquals(403, $response['status']);
}
/**
* Tests security token endpoint protection.
*/
public function testSecurityTokenEndpointProtection(): void {
$this->drupalLogin($this->testUser);
// Test 1: Valid referer should work.
$this->drupalGet('/media-api/security-tokens', [], [
'Referer' => $this->baseUrl . '/admin/content',
]);
$this->assertSession()->statusCodeEquals(200);
// Test 2: External referer should be blocked.
$this->drupalGet('/media-api/security-tokens', [], [
'Referer' => 'https://evil.com/attack',
]);
$this->assertSession()->statusCodeEquals(403);
// Test 3: No referer should be blocked.
$this->drupalGet('/media-api/security-tokens', [], []);
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests comprehensive security validation sequence.
*/
public function testComprehensiveSecurityValidation(): void {
$this->drupalLogin($this->testUser);
// Get valid tokens.
$token_response = $this->drupalGet('/media-api/security-tokens', [], [
'Referer' => $this->baseUrl . '/admin/content',
]);
$this->assertSession()->statusCodeEquals(200);
$tokens = json_decode($token_response, TRUE);
// Create a real file for testing.
$file = File::create([
'uri' => 'public://security-test.jpg',
'filename' => 'security-test.jpg',
'filemime' => 'image/jpeg',
'status' => 1,
]);
// Create actual file content.
$directory = dirname($file->getFileUri());
$this->container->get('file_system')->prepareDirectory($directory, 1);
file_put_contents($file->getFileUri(), 'fake-image-content');
$file->save();
// Test with all valid security parameters.
$payload = [
'uuid' => $file->uuid(),
'csrf_token' => $tokens['csrf_token'],
'user_uuid' => $tokens['user_uuid'],
];
$response = $this->postJsonRequest('/media-api/swap-file-to-media/file-uuid', $payload, [
'Origin' => $this->baseUrl,
'Content-Type' => 'application/json',
]);
// Should pass all security checks and attempt file processing.
$this->assertEquals(403, $response['status'], 'All valid security parameters should pass validation');
}
/**
* Helper method to make JSON POST requests.
*/
protected function postJsonRequest(string $url, array $data, array $headers = []): array {
$default_headers = [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
$headers = array_merge($default_headers, $headers);
$client = $this->getHttpClient();
try {
$response = $client->post($this->buildUrl($url), [
RequestOptions::JSON => $data,
RequestOptions::HEADERS => $headers,
RequestOptions::HTTP_ERRORS => FALSE,
]);
return [
'status' => $response->getStatusCode(),
'body' => $response->getBody()->getContents(),
'headers' => $response->getHeaders(),
];
}
catch (\Exception $e) {
return [
'status' => 500,
'body' => $e->getMessage(),
'headers' => [],
];
}
}
/**
* {@inheritdoc}
*/
public function shutDown(): void {
// If a user is logged in, log them out to clean up session.
if ($this->loggedInUser) {
$this->drupalLogout();
}
}
}
