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

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

declare(strict_types=1);

namespace Drupal\Tests\image_to_media_swapper\Kernel;

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

/**
 * Tests for specific security vulnerabilities and attack vectors.
 *
 * @group image_to_media_swapper
 * @group security
 */
class SecurityVulnerabilityTest extends KernelTestBase {

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

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

  /**
   * The swapper service.
   */
  protected SwapperService $swapperService;

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

  /**
   * Tests protection against SSRF attacks targeting internal services.
   */
  public function testSsrfAttackVectors(): void {
    $ssrfTargets = [
      // Localhost variations.
      'http://127.0.0.1:80/file.jpg',
      'http://127.0.0.1:22/file.jpg',
      'http://127.0.0.1:3306/file.jpg',
      'http://127.0.0.1:6379/file.jpg',
      'http://127.0.0.1:9000/file.jpg',
      'http://localhost:8080/file.jpg',

      // Private network ranges.
      'http://192.168.1.1/file.jpg',
      'http://10.0.0.1/file.jpg',
      'http://172.16.1.1/file.jpg',

      // Link-local addresses.
      'http://169.254.1.1/file.jpg',
      'http://169.254.169.254/latest/meta-data/',

      // Reserved addresses.
      'http://0.0.0.0/file.jpg',
      'http://255.255.255.255/file.jpg',

      // IPv6 localhost.
      'http://[::1]/file.jpg',
      'http://[::ffff:127.0.0.1]/file.jpg',
    ];

    foreach ($ssrfTargets as $target) {
      $errors = $this->securityValidationService->validateUrl($target);
      $this->assertNotEmpty($errors, "SSRF target should be blocked: {$target}");
      $this->assertContains('Private/internal IP addresses are not allowed', $errors,
        "Expected SSRF protection error for: {$target}");
    }
  }

  /**
   * Tests protection against malicious file types and MIME type spoofing.
   */
  public function testMaliciousFileTypes(): void {
    $maliciousTypes = [
      // Executable files.
      ['application/x-executable', 'malware.exe'],
      ['application/x-msdos-program', 'virus.com'],
      ['application/x-msdownload', 'trojan.exe'],

      // Script files.
      ['text/x-php', 'shell.php'],
      ['application/x-httpd-php', 'backdoor.php'],
      ['text/x-python', 'script.py'],
      ['application/javascript', 'malicious.js'],

      // Archive files that could contain malicious content.
      ['application/zip', 'payload.zip'],
      ['application/x-tar', 'exploit.tar'],
      ['application/x-7z-compressed', 'malware.7z'],

      // Potentially dangerous document types.
      ['application/vnd.ms-excel.sheet.macroEnabled.12', 'macro.xlsm'],
      ['application/vnd.ms-word.document.macroEnabled.12', 'macro.docm'],

      // Web content that could be dangerous if served.
      ['text/html', 'xss.html'],
      ['application/xhtml+xml', 'exploit.xhtml'],

      // MIME type spoofing attempts.
      ['image/jpeg', '../../../etc/passwd'],
      ['image/png', 'shell.php.png'],
      ['text/plain', 'exploit.txt.exe'],
    ];

    foreach ($maliciousTypes as [$mimeType, $filename]) {
      $errors = $this->securityValidationService->validateFileType($mimeType, $filename);
      $this->assertNotEmpty($errors, "Malicious file type should be blocked: {$mimeType} - {$filename}");
    }
  }

  /**
   * Tests protection against path traversal attacks in domain validation.
   */
  public function testDomainPathTraversalAttacks(): void {
    // Enable domain restriction with a safe domain.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('restrict_domains', TRUE);
    $config->set('allowed_domains', ['safe.example.com']);
    $config->save();

    $pathTraversalAttempts = [
      'https://safe.example.com/../../../etc/passwd',
      'https://safe.example.com/..%2f..%2f..%2fetc%2fpasswd',
      'https://safe.example.com/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
      'https://safe.example.com/file.jpg?param=../../../../etc/passwd',
      'https://safe.example.com/file.jpg#../../../../etc/passwd',
    // Username attempt.
      'https://evil.com@safe.example.com/file.jpg',
    ];

    foreach ($pathTraversalAttempts as $url) {
      // These should pass domain validation but may have path issues
      // The important thing is they don't crash the validator.
      $errors = $this->securityValidationService->validateUrl($url);
      $this->assertIsArray($errors, "Path traversal URL should not crash validator: {$url}");
    }
  }

  /**
   * Tests protection against DNS rebinding attacks.
   */
  public function testDnsRebindingProtection(): void {
    // Test domains that might resolve to private IPs.
    $suspiciousDomains = [
    // Often resolves to 127.0.0.1.
      'http://localtest.me/file.jpg',
    // Often resolves to 127.0.0.1.
      'http://lvh.me/file.jpg',
    // Service that resolves to the IP.
      'http://127.0.0.1.nip.io/file.jpg',
      'http://192-168-1-1.nip.io/file.jpg',
    ];

    foreach ($suspiciousDomains as $domain) {
      $errors = $this->securityValidationService->validateUrl($domain);
      // Note: These might pass domain validation but should fail IP validation
      // if they resolve to private IPs. The exact behavior depends on DNS
      // resolution.
      $this->assertIsArray($errors, "DNS rebinding domain should not crash: {$domain}");
    }
  }

  /**
   * Tests file size boundary attacks and integer overflow protection.
   */
  public function testFileSizeBoundaryAttacks(): void {
    // Set a reasonable limit for testing.
    $config = $this->config('image_to_media_swapper.security_settings');
    // 10MB
    $config->set('max_file_size', 10)->save();

    $boundaryTests = [
      // Just under limit (should pass)
      (10 * 1024 * 1024) - 1,

      // Just over limit (should fail)
      (10 * 1024 * 1024) + 1,

      // Way over limit (should fail)
      100 * 1024 * 1024,

      // Potential integer overflow attempts.
      PHP_INT_MAX,
      PHP_INT_MAX - 1,

      // Edge cases.
      0,
    // Negative sizes should be handled gracefully.
      -1,
    ];

    foreach ($boundaryTests as $size) {
      $errors = $this->securityValidationService->validateFileSize($size);
      $this->assertIsArray($errors, "File size validation should not crash for size: {$size}");

      if ($size < 0) {
        // Negative sizes should probably be rejected, but shouldn't crash.
        $this->assertNotEmpty($errors, "Negative file size should be rejected: {$size}");
      }
      elseif ($size > 10 * 1024 * 1024) {
        $this->assertNotEmpty($errors, "Oversized file should be rejected: {$size}");
      }
    }
  }

  /**
   * Tests protocol confusion attacks.
   */
  public function testProtocolConfusionAttacks(): void {
    $protocolAttacks = [
      // Non-HTTP protocols that might be dangerous.
      'file:///etc/passwd',
      'ftp://internal.server.com/file.jpg',
      'gopher://127.0.0.1:70/file.jpg',
      'ldap://internal.ldap.server/file.jpg',
      'dict://127.0.0.1:2628/file.jpg',

      // Protocol tricks.
      'jar:http://example.com/file.jar!/file.jpg',
      'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ==',

      // Case variations.
      'HTTP://example.com/file.jpg',
      'hTTp://example.com/file.jpg',
      'HTTPS://example.com/file.jpg',
    ];

    foreach ($protocolAttacks as $url) {
      $errors = $this->securityValidationService->validateUrl($url);

      if (strtolower(parse_url($url, PHP_URL_SCHEME)) === 'http' ||
          strtolower(parse_url($url, PHP_URL_SCHEME)) === 'https') {
        // Case variations of HTTP/HTTPS might be allowed depending on
        // implementation.
        $this->assertIsArray($errors, "Protocol variation should not crash: {$url}");
      }
      else {
        // Non-HTTP protocols should be blocked.
        $this->assertNotEmpty($errors, "Non-HTTP protocol should be blocked: {$url}");
        $this->assertContains('Only HTTP and HTTPS protocols are allowed', $errors);
      }
    }
  }

  /**
   * Tests wildcard domain bypass attempts.
   */
  public function testWildcardDomainBypassAttacks(): void {
    // Configure wildcard domain and disable IP validation for this test.
    $config = $this->config('image_to_media_swapper.security_settings');
    $config->set('restrict_domains', TRUE);
    $config->set('allowed_domains', ['*.trusted.com']);
    // Disable IP validation for this test.
    $config->set('block_private_ips', FALSE);
    $config->save();

    $allowedDomains = ['subdomain.trusted.com', 'trusted.com'];
    $blockedDomains = [
      'evil.com.trusted.com.evil.com',
      'trusted.com.evil.com',
      'notrusted.com',
      'trusted.com.evil.example.com',
    ];

    // Test allowed domains pass.
    foreach ($allowedDomains as $domain) {
      $url = "https://{$domain}/file.jpg";
      $errors = $this->securityValidationService->validateUrl($url);
      $this->assertEmpty($errors, "Legitimate wildcard domain should be allowed: {$url}");
    }

    // Test blocked domains fail.
    foreach ($blockedDomains as $domain) {
      $url = "https://{$domain}/file.jpg";
      $errors = $this->securityValidationService->validateUrl($url);
      $this->assertNotEmpty($errors, "Wildcard bypass attempt should be blocked: {$url}");
    }
  }

  /**
   * Tests file URL validation security.
   */
  public function testFileUrlValidationSecurity(): void {
    $maliciousFileUrls = [
      // SSRF attacks targeting internal services.
      'http://127.0.0.1:22/malicious.pdf',
      'http://localhost:3306/exploit.pdf',
      'http://192.168.1.1/internal.pdf',
      'http://169.254.169.254/metadata.pdf',

      // Non-HTTP protocols.
      'file:///etc/passwd.pdf',
      'ftp://internal.server.com/secret.pdf',
      'gopher://127.0.0.1:70/payload.pdf',

      // Path traversal attempts.
      'https://safe.example.com/../../../etc/passwd.pdf',
      'https://safe.example.com/file.pdf?param=../../../../etc/passwd',

      // Domain bypass attempts.
      'https://evil.com@safe.example.com/file.pdf',
      'https://evil.com.safe.example.com/file.pdf',
    ];

    foreach ($maliciousFileUrls as $url) {
      $errors = $this->securityValidationService->validateUrl($url);
      $this->assertNotEmpty($errors, "Malicious file URL should be blocked: {$url}");
    }
  }

  /**
   * Tests filename sanitization security.
   */
  public function testFilenameSanitizationSecurity(): void {
    $maliciousFilenames = [
      'https://example.com/../../etc/passwd.pdf',
      'https://example.com/file with spaces.pdf',
      'https://example.com/file"with"quotes.pdf',
      'https://example.com/file<script>.pdf',
      'https://example.com/file|pipe.pdf',
      'https://example.com/file?query=value.pdf',
      'https://example.com/file#fragment.pdf',
      'https://example.com/very-long-filename-that-exceeds-reasonable-limits-and-could-cause-issues-with-filesystem-operations-or-database-storage-constraints.pdf',
    ];

    $reflection = new \ReflectionClass($this->swapperService);
    $generateSafeFilename = $reflection->getMethod('generateSafeFilename');
    $generateSafeFilename->setAccessible(TRUE);

    foreach ($maliciousFilenames as $url) {
      $safeFilename = $generateSafeFilename->invoke($this->swapperService, $url);

      // Ensure the filename is safe.
      $this->assertLessThanOrEqual(100, strlen($safeFilename), "Filename should not exceed 100 characters: {$safeFilename}");
      $this->assertDoesNotMatchRegularExpression('/[^a-zA-Z0-9._-]/', $safeFilename, "Filename should only contain safe characters: {$safeFilename}");
      $this->assertStringNotContainsString('..', $safeFilename, "Filename should not contain path traversal: {$safeFilename}");
      $this->assertNotEmpty($safeFilename, "Filename should not be empty: {$safeFilename}");
      $this->assertStringContainsString('.', $safeFilename, "Filename should have an extension: {$safeFilename}");
    }
  }

  /**
   * Tests file MIME type validation security.
   */
  public function testFileMimeTypeValidationSecurity(): void {
    $maliciousMimeTypes = [
      // Executable MIME types disguised as allowed files.
      ['application/x-executable', 'malware.pdf'],
      ['application/x-msdownload', 'trojan.pdf'],
      ['text/x-php', 'shell.pdf'],
      ['application/javascript', 'xss.pdf'],

      // Archive types that could contain malicious PDFs.
      ['application/zip', 'payload.pdf'],
      ['application/x-tar', 'exploit.pdf'],

      // HTML/XML types that could be dangerous.
      ['text/html', 'xss.pdf'],
      ['application/xhtml+xml', 'exploit.pdf'],
      ['text/xml', 'xxe.pdf'],
    ];

    foreach ($maliciousMimeTypes as [$mimeType, $filename]) {
      $errors = $this->securityValidationService->validateFileType($mimeType, $filename);
      $this->assertNotEmpty($errors, "Malicious MIME type should be blocked: {$mimeType} - {$filename}");
    }

    // Test legitimate file MIME type passes.
    $errors = $this->securityValidationService->validateFileType('application/pdf', 'document.pdf');
    // This might pass or fail depending on configuration, but shouldn't crash.
    $this->assertIsArray($errors, 'File MIME type validation should not crash');
  }

}

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

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