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');
}
}
