image_to_media_swapper-2.x-dev/tests/src/Kernel/SecurityValidationServiceTest.php

tests/src/Kernel/SecurityValidationServiceTest.php
<?php

declare(strict_types=1);

namespace Drupal\Tests\image_to_media_swapper\Kernel;

use Drupal\image_to_media_swapper\SecurityValidationService;
use Drupal\KernelTests\KernelTestBase;

/**
 * Tests the SecurityValidationService.
 *
 * @group image_to_media_swapper
 * @coversDefaultClass \Drupal\image_to_media_swapper\SecurityValidationService
 */
class SecurityValidationServiceTest extends KernelTestBase {

  /**
   * {@inheritdoc}
   */
  protected static $modules = [
    'image_to_media_swapper',
    'system',
  ];

  /**
   * The security validation service.
   */
  protected SecurityValidationService $securityValidationService;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();
    $this->installConfig(['image_to_media_swapper']);
    $this->securityValidationService = $this->container->get('image_to_media_swapper.security_validation');
  }

  /**
   * Tests URL validation with default settings.
   *
   * @covers ::validateUrl
   */
  public function testUrlValidationDefaults(): void {
    // Valid HTTPS URL should pass.
    $errors = $this->securityValidationService->validateUrl('https://example.com/file.jpg');
    $this->assertEmpty($errors);

    // Valid HTTP URL should pass (default allows HTTP).
    $errors = $this->securityValidationService->validateUrl('http://example.com/file.jpg');
    $this->assertEmpty($errors);

    // Invalid URL format should fail.
    $errors = $this->securityValidationService->validateUrl('not-a-url');
    $this->assertNotEmpty($errors);
    $this->assertContains('Invalid URL format', $errors);

    // Invalid protocol should fail.
    $errors = $this->securityValidationService->validateUrl('ftp://example.com/file.jpg');
    $this->assertNotEmpty($errors);
    $this->assertContains('Only HTTP and HTTPS protocols are allowed', $errors);
  }

  /**
   * Tests SSRF protection against private IPs.
   *
   * @covers ::validateUrl
   */
  public function testSsrfProtection(): void {
    // Test private IP ranges are blocked by default.
    $privateIPs = [
      'http://192.168.1.1/file.jpg',
      'http://10.0.0.1/file.jpg',
      'http://172.16.0.1/file.jpg',
      'http://127.0.0.1/file.jpg',
      'http://localhost/file.jpg',
    ];

    foreach ($privateIPs as $url) {
      $errors = $this->securityValidationService->validateUrl($url);
      $this->assertNotEmpty($errors, "Private IP should be blocked: {$url}");
      $this->assertContains('Private/internal IP addresses are not allowed', $errors);
    }

    // Test that disabling IP blocking allows private IPs.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('block_private_ips', FALSE)->save();

    $errors = $this->securityValidationService->validateUrl('http://192.168.1.1/file.jpg');
    $this->assertEmpty($errors);
  }

  /**
   * Tests HTTPS requirement enforcement.
   *
   * @covers ::validateUrl
   */
  public function testHttpsRequirement(): void {
    // Enable HTTPS requirement.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('require_https', TRUE)->save();

    // HTTP should now fail.
    $errors = $this->securityValidationService->validateUrl('http://example.com/file.jpg');
    $this->assertNotEmpty($errors);
    $this->assertContains('HTTPS is required', $errors);

    // HTTPS should still pass.
    $errors = $this->securityValidationService->validateUrl('https://example.com/file.jpg');
    $this->assertEmpty($errors);
  }

  /**
   * Tests domain restriction functionality.
   *
   * @covers ::validateUrl
   */
  public function testDomainRestriction(): void {
    // Enable domain restriction.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('restrict_domains', TRUE);
    $config->set('allowed_domains', ['example.com', '*.trusted.org']);
    $config->save();

    // Allowed exact domain should pass.
    $errors = $this->securityValidationService->validateUrl('https://example.com/file.jpg');
    $this->assertEmpty($errors);

    // Wildcard subdomain should pass.
    $errors = $this->securityValidationService->validateUrl('https://cdn.trusted.org/file.jpg');
    $this->assertEmpty($errors);

    // Base domain of wildcard should pass.
    $errors = $this->securityValidationService->validateUrl('https://trusted.org/file.jpg');
    $this->assertEmpty($errors);

    // Non-allowed domain should fail.
    $errors = $this->securityValidationService->validateUrl('https://malicious.com/file.jpg');
    $this->assertNotEmpty($errors);
    $this->assertContains('Domain is not in the allowed list', $errors);

    // Subdomain of non-wildcard should fail.
    $errors = $this->securityValidationService->validateUrl('https://sub.example.com/file.jpg');
    $this->assertNotEmpty($errors);
    $this->assertContains('Domain is not in the allowed list', $errors);
  }

  /**
   * Tests file type validation.
   *
   * @covers ::validateFileType
   */
  public function testFileTypeValidation(): void {
    // Configure limited file types using the correct array fields.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('allowed_extensions_array', ['jpg', 'png']);
    $config->set('allowed_mime_types_array', ['image/jpeg', 'image/png']);
    $config->save();

    // Allowed extension and MIME type should pass.
    $errors = $this->securityValidationService->validateFileType('image/jpeg', 'test.jpg');
    $this->assertEmpty($errors);

    // Disallowed extension should fail.
    $errors = $this->securityValidationService->validateFileType('image/gif', 'test.gif');
    $this->assertNotEmpty($errors);
    $this->assertStringContainsString("File extension 'gif' is not in the allowed list", implode(', ', $errors));

    // Disallowed MIME type should fail.
    $errors = $this->securityValidationService->validateFileType('application/pdf', 'test.jpg');
    $this->assertNotEmpty($errors);
    $this->assertStringContainsString("MIME type 'application/pdf' is not in the allowed list", implode(', ', $errors));

    // Dangerous MIME type should always fail.
    $errors = $this->securityValidationService->validateFileType('text/html', 'malicious.jpg');
    $this->assertNotEmpty($errors);
    $this->assertStringContainsString("MIME type 'text/html' is blocked for security reasons", implode(', ', $errors));
  }

  /**
   * Tests file size validation.
   *
   * @covers ::validateFileSize
   */
  public function testFileSizeValidation(): void {
    // Set 5MB limit.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('max_file_size', 5)->save();

    // File under 3MB limit should pass.
    $errors = $this->securityValidationService->validateFileSize(3 * 1024 * 1024);
    $this->assertEmpty($errors);

    // File over 10MB limit should fail.
    $errors = $this->securityValidationService->validateFileSize(10 * 1024 * 1024);
    $this->assertNotEmpty($errors);
    $this->assertStringContainsString('exceeds maximum allowed size', $errors[0]);
  }

  /**
   * Tests Guzzle options generation.
   *
   * @covers ::getGuzzleOptions
   */
  public function testGuzzleOptions(): void {
    $options = $this->securityValidationService->getGuzzleOptions();

    // Check default timeout.
    $this->assertEquals(30, $options['timeout']);
    $this->assertEquals(5, $options['connect_timeout']);

    // Check redirect settings.
    $this->assertEquals(3, $options['allow_redirects']['max']);
    $this->assertTrue($options['allow_redirects']['strict']);
    $this->assertFalse($options['allow_redirects']['referer']);

    // Check protocols include both HTTP and HTTPS by default.
    $this->assertEquals(['http', 'https'], $options['allow_redirects']['protocols']);

    // Test HTTPS-only mode.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('require_https', TRUE)->save();

    $options = $this->securityValidationService->getGuzzleOptions();
    $this->assertEquals(['https'], $options['allow_redirects']['protocols']);
  }

  /**
   * Tests security boundary conditions.
   *
   * @covers ::validateUrl
   * @covers ::validateFileType
   * @covers ::validateFileSize
   */
  public function testSecurityBoundaryConditions(): void {
    // Test edge cases for URL validation.
    $edgeCaseUrls = [
      'https://example.com/../../../etc/passwd',
      'https://example.com/file.jpg?param=../../etc/passwd',
      'https://example.com:80/../file.jpg',
      'https://user:pass@example.com/file.jpg',
    ];

    foreach ($edgeCaseUrls as $url) {
      $errors = $this->securityValidationService->validateUrl($url);
      // Should not crash, may or may not pass depending on URL structure.
      $this->assertIsArray($errors);
    }

    // Test empty/null inputs.
    $errors = $this->securityValidationService->validateFileType('', '');
    $this->assertIsArray($errors);

    $errors = $this->securityValidationService->validateFileSize(0);
    // Zero size should be allowed.
    $this->assertEmpty($errors);

    // Test maximum integer file size.
    $errors = $this->securityValidationService->validateFileSize(PHP_INT_MAX);
    // Should exceed any reasonable limit.
    $this->assertNotEmpty($errors);
  }

  /**
   * Tests performance with concurrent URL validations.
   *
   * @covers ::validateUrl
   */
  public function testConcurrentUrlValidationPerformance(): void {
    $urls_to_test = [
      'https://example.com/file1.jpg',
      'https://test.org/file2.png',
      'https://secure.site.com/file3.gif',
    // Should be blocked.
      'http://192.168.1.1/malicious.jpg',
      'https://evil.com/file4.jpg',
    ];

    $start_time = microtime(TRUE);
    $results = [];

    // Simulate concurrent processing by rapidly validating URLs.
    foreach ($urls_to_test as $url) {
      $results[$url] = $this->securityValidationService->validateUrl($url);
    }

    $end_time = microtime(TRUE);
    $execution_time = $end_time - $start_time;

    // Validation should complete quickly (under 1 second for 5 URLs).
    $this->assertLessThan(1.0, $execution_time, 'URL validation should be performant');

    // Verify results are correct.
    $this->assertEmpty($results['https://example.com/file1.jpg'], 'Valid URL should pass');
    $this->assertNotEmpty($results['http://192.168.1.1/malicious.jpg'], 'Private IP should be blocked');

    // Ensure all results are arrays.
    foreach ($results as $url => $errors) {
      $this->assertIsArray($errors, "Results for {$url} should be array");
    }
  }

  /**
   * Tests file size validation under stress conditions.
   *
   * @covers ::validateFileSize
   */
  public function testFileSizeValidationStress(): void {
    $config = $this->config('image_to_media_swapper.security_settings');
    // 10MB limit
    $config->set('max_file_size', 10)->save();

    $test_sizes = [
      // Normal sizes (should pass).
    // 1KB.
      1024,
    // 1MB
      1024 * 1024,
    // 5MB
      5 * 1024 * 1024,

      // Boundary sizes.
    // Just under limit.
      (10 * 1024 * 1024) - 1,
    // Exactly at limit.
      10 * 1024 * 1024,
    // Just over limit.
      (10 * 1024 * 1024) + 1,

      // Stress sizes.
    // 100MB.
      100 * 1024 * 1024,
    // 1GB
      1024 * 1024 * 1024,
    // Maximum integer.
      PHP_INT_MAX,

      // Edge cases.
      0,
      -1,
    ];

    $start_time = microtime(TRUE);
    $results = [];

    foreach ($test_sizes as $size) {
      $results[$size] = $this->securityValidationService->validateFileSize($size);
    }

    $end_time = microtime(TRUE);
    $execution_time = $end_time - $start_time;

    // File size validation should be extremely fast.
    $this->assertLessThan(0.1, $execution_time, 'File size validation should be very fast');

    // Verify boundary conditions.
    $this->assertEmpty($results[5 * 1024 * 1024], '5MB should pass');
    $this->assertEmpty($results[(10 * 1024 * 1024) - 1], 'Just under limit should pass');
    $this->assertNotEmpty($results[(10 * 1024 * 1024) + 1], 'Just over limit should fail');
    $this->assertNotEmpty($results[100 * 1024 * 1024], '100MB should fail');
    $this->assertNotEmpty($results[-1], 'Negative size should fail');
  }

  /**
   * Tests bulk file type validation for performance.
   *
   * @covers ::validateFileType
   */
  public function testBulkFileTypeValidationPerformance(): void {
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('allowed_extensions_array', ['jpg', 'png', 'gif']);
    $config->set('allowed_mime_types_array', ['image/jpeg', 'image/png', 'image/gif']);
    $config->save();

    $test_files = [
      // Valid files.
      ['image/jpeg', 'photo.jpg'],
      ['image/png', 'screenshot.png'],
      ['image/gif', 'animation.gif'],

      // Invalid files.
      ['application/pdf', 'document.pdf'],
      ['text/html', 'malicious.html'],
      ['application/x-executable', 'virus.exe'],
      ['text/x-php', 'shell.php'],

      // Edge cases.
      ['', ''],
      ['image/jpeg', ''],
      ['', 'file.jpg'],
    ];

    $start_time = microtime(TRUE);
    $results = [];

    foreach ($test_files as [$mime, $filename]) {
      $key = "{$mime}:{$filename}";
      $results[$key] = $this->securityValidationService->validateFileType($mime, $filename);
    }

    $end_time = microtime(TRUE);
    $execution_time = $end_time - $start_time;

    // File type validation should be fast.
    $this->assertLessThan(0.5, $execution_time, 'Bulk file type validation should be performant');

    // Verify some key results.
    $this->assertEmpty($results['image/jpeg:photo.jpg'], 'Valid JPEG should pass');
    $this->assertNotEmpty($results['text/html:malicious.html'], 'HTML should be blocked');
    $this->assertNotEmpty($results['application/x-executable:virus.exe'], 'Executable should be blocked');
  }

  /**
   * Tests memory usage during intensive validation operations.
   *
   * @covers ::validateUrl
   * @covers ::validateFileType
   * @covers ::validateFileSize
   */
  public function testMemoryUsageDuringIntensiveOperations(): void {
    $initial_memory = memory_get_usage(TRUE);

    // Generate many validation operations.
    for ($i = 0; $i < 1000; $i++) {
      // URL validation.
      $this->securityValidationService->validateUrl("https://example{$i}.com/file.jpg");

      // File type validation.
      $this->securityValidationService->validateFileType('image/jpeg', "file{$i}.jpg");

      // File size validation.
      $this->securityValidationService->validateFileSize($i * 1024);
    }

    $final_memory = memory_get_usage(TRUE);
    $memory_increase = $final_memory - $initial_memory;

    // Memory usage should not increase dramatically (less than 10MB).
    $this->assertLessThan(10 * 1024 * 1024, $memory_increase,
      'Memory usage should not increase dramatically during intensive operations');
  }

  /**
   * Tests validation consistency under repeated operations.
   *
   * @covers ::validateUrl
   * @covers ::validateFileType
   * @covers ::validateFileSize
   */
  public function testValidationConsistency(): void {
    $test_cases = [
      ['url' => 'https://example.com/test.jpg'],
      ['mime' => 'image/jpeg', 'filename' => 'test.jpg'],
    // 5MB
      ['size' => 5 * 1024 * 1024],
    ];

    foreach ($test_cases as $test_case) {
      $results = [];

      // Run the same validation 10 times.
      for ($i = 0; $i < 10; $i++) {
        if (isset($test_case['url'])) {
          $results[] = $this->securityValidationService->validateUrl($test_case['url']);
        }
        elseif (isset($test_case['mime'])) {
          $results[] = $this->securityValidationService->validateFileType($test_case['mime'], $test_case['filename']);
        }
        elseif (isset($test_case['size'])) {
          $results[] = $this->securityValidationService->validateFileSize($test_case['size']);
        }
      }

      // All results should be identical.
      $first_result = $results[0];
      foreach ($results as $i => $result) {
        $this->assertEquals($first_result, $result,
          "Validation result {$i} should be consistent with first result");
      }
    }
  }

  /**
   * Tests behavior under malformed input attacks.
   *
   * @covers ::validateUrl
   * @covers ::validateFileType
   */
  public function testMalformedInputHandling(): void {
    $malformed_urls = [
      // Malformed URLs that might cause issues.
      'http://',
      'https://',
      '://example.com',
      'http://.',
      'http://256.256.256.256/',
      'http://example..com/',
      'http://-example.com/',
      'http://example-.com/',
    // Very long URL.
      str_repeat('a', 10000) . '.com',
    // Null bytes.
      "http://example.com/\x00\x01\x02",
    ];

    foreach ($malformed_urls as $url) {
      try {
        $errors = $this->securityValidationService->validateUrl($url);
        $this->assertIsArray($errors, "Malformed URL should not crash validator: {$url}");
      }
      catch (\Exception $e) {
        $this->fail("Malformed URL caused exception: {$url} - " . $e->getMessage());
      }
    }

    $malformed_files = [
      // Malformed MIME types and filenames.
      ["\x00\x01\x02", "file.jpg"],
      ["image/jpeg", "\x00\x01\x02"],
    // Very long MIME type.
      [str_repeat('a', 1000), "file.jpg"],
    // Very long filename.
      ["image/jpeg", str_repeat('b', 1000)],
      ["", ""],
      [NULL, NULL],
    ];

    foreach ($malformed_files as [$mime, $filename]) {
      try {
        $errors = $this->securityValidationService->validateFileType($mime, $filename);
        $this->assertIsArray($errors, "Malformed file type should not crash validator");
      }
      catch (\TypeError $e) {
        // NULL values might cause TypeErrors, which is acceptable.
        $this->assertStringContainsString('null', strtolower($e->getMessage()));
      }
      catch (\Exception $e) {
        $this->fail("Malformed file type caused unexpected exception: " . $e->getMessage());
      }
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc