├── .github └── workflows │ └── build.yml ├── LICENSE ├── README.md ├── composer.json └── src ├── Engine ├── EngineInterface.php ├── PngBompEngine.php ├── RarBombEngine.php └── ZipBombEngine.php └── Scanner ├── BombScanner.php └── BombScannerResult.php /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | run: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: [ ubuntu-latest ] 11 | php-versions: [ '8.1', '8.2' ] 12 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v1 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | extensions: xml, pcov, mbstring, pdo, pdo_mysql, intl, zip 23 | coverage: none 24 | 25 | - name: Check PHP Version 26 | run: php -v 27 | 28 | - name: Check Composer Version 29 | run: composer -V 30 | 31 | - name: Check PHP Extensions 32 | run: php -m 33 | 34 | - name: Validate composer.json and composer.lock 35 | run: composer validate 36 | 37 | - name: Install dependencies 38 | run: composer update --prefer-dist --no-progress --no-suggest 39 | 40 | - name: Run test suite 41 | run: composer test:all 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 odan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # selective/archive-bomb-scanner 2 | 3 | ZIP and PNG bomb scanner for PHP. 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/github/release/selective-php/archive-bomb-scanner.svg?style=flat-square)](https://packagist.org/packages/selective/archive-bomb-scanner) 6 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 7 | [![Build Status](https://github.com/selective-php/archive-bomb-scanner/workflows/build/badge.svg)](https://github.com/selective-php/archive-bomb-scanner/actions) 8 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/selective-php/archive-bomb-scanner.svg?style=flat-square)](https://scrutinizer-ci.com/g/selective-php/archive-bomb-scanner/code-structure) 9 | [![Quality Score](https://img.shields.io/scrutinizer/quality/g/selective-php/archive-bomb-scanner.svg?style=flat-square)](https://scrutinizer-ci.com/g/selective-php/archive-bomb-scanner/?branch=master) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/selective/archive-bomb-scanner.svg?style=flat-square)](https://packagist.org/packages/selective/archive-bomb-scanner/stats) 11 | 12 | ## Features 13 | 14 | * Detection of ZIP archive bombs 15 | * Detection of RAR archive bombs 16 | * Detection of PNG bombs 17 | * No dependencies 18 | * Very fast 19 | 20 | ## Requirements 21 | 22 | * PHP 8.1+ 23 | 24 | ## Installation 25 | 26 | ``` 27 | composer require selective/archive-bomb-scanner 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Scan ZIP file 33 | 34 | ```php 35 | use Selective\ArchiveBomb\Scanner\BombScanner; 36 | use Selective\ArchiveBomb\Engine\ZipBombEngine; 37 | use SplFileObject; 38 | 39 | $file = new SplFileObject('42.zip'); 40 | 41 | $scanner = new BombScanner(); 42 | $scanner->addEngine(new ZipBombEngine()); 43 | 44 | $scannerResult = $scanner->scanFile($file); 45 | 46 | if ($scannerResult->isBomb()) { 47 | echo 'Archive bomb detected!'; 48 | } else { 49 | echo 'File is clean'; 50 | } 51 | ``` 52 | 53 | ### Scan in-memory ZIP file 54 | 55 | ```php 56 | use Selective\ArchiveBomb\BombScanner; 57 | use Selective\ArchiveBomb\Engine\ZipBombEngine; 58 | use SplTempFileObject; 59 | 60 | $file = new SplTempFileObject(); 61 | 62 | $file->fwrite('my file content'); 63 | 64 | $scanner = new BombScanner(); 65 | $scanner->addEngine(new ZipBombEngine()); 66 | 67 | $isBomb = $detector->scanFile($file)->isBomb(); // true or false 68 | ``` 69 | 70 | ### Scan RAR file 71 | 72 | ```php 73 | use Selective\ArchiveBomb\Scanner\BombScanner; 74 | use Selective\ArchiveBomb\Engine\RarBombEngine; 75 | use SplFileObject; 76 | 77 | $file = new SplFileObject('10GB.rar'); 78 | 79 | $scanner = new BombScanner(); 80 | $scanner->addEngine(new RarBombEngine()); 81 | 82 | $scannerResult = $scanner->scanFile($file); 83 | 84 | if ($scannerResult->isBomb()) { 85 | echo 'Archive bomb detected!'; 86 | } else { 87 | echo 'File is clean'; 88 | } 89 | ``` 90 | 91 | ### Scan PNG file 92 | 93 | ```php 94 | use Selective\ArchiveBomb\Scanner\BombScanner; 95 | use Selective\ArchiveBomb\Engine\PngBombEngine; 96 | use SplFileObject; 97 | 98 | $file = new SplFileObject('example.png'); 99 | 100 | $scanner = new BombScanner(); 101 | $scanner->addEngine(new PngBombEngine()); 102 | 103 | $scannerResult = $scanner->scanFile($file); 104 | 105 | if ($scannerResult->isBomb()) { 106 | echo 'PNG bomb detected!'; 107 | } else { 108 | echo 'File is clean'; 109 | } 110 | ``` 111 | 112 | ## License 113 | 114 | MIT 115 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selective/archive-bomb-scanner", 3 | "description": "ZIP and PNG bomb scanner", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "archive-bomb", 8 | "zip-bomb", 9 | "scanner", 10 | "zip", 11 | "png", 12 | "png-bomb" 13 | ], 14 | "homepage": "https://github.com/selective-php/archive-bomb-scanner", 15 | "require": { 16 | "php": "^8.1" 17 | }, 18 | "require-dev": { 19 | "friendsofphp/php-cs-fixer": "^3", 20 | "phpstan/phpstan": "^1", 21 | "phpunit/phpunit": "^10", 22 | "selective/rar": "^0.2 || ^0.3", 23 | "squizlabs/php_codesniffer": "^3" 24 | }, 25 | "suggest": { 26 | "ext-zip": "Use this extension to detect ZIP archive bombs", 27 | "selective/rar": "Use this package to detect RAR archive bombs" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Selective\\ArchiveBomb\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Selective\\ArchiveBomb\\Test\\": "tests" 37 | } 38 | }, 39 | "config": { 40 | "process-timeout": 0, 41 | "sort-packages": true 42 | }, 43 | "scripts": { 44 | "cs:check": "php-cs-fixer fix --dry-run --format=txt --verbose --diff --config=.cs.php --ansi", 45 | "cs:fix": "php-cs-fixer fix --config=.cs.php --ansi", 46 | "sniffer:check": "phpcs --standard=phpcs.xml", 47 | "sniffer:fix": "phpcbf --standard=phpcs.xml", 48 | "stan": "phpstan analyse -c phpstan.neon --no-progress --ansi --xdebug", 49 | "test": "phpunit --configuration phpunit.xml --do-not-cache-result --colors=always", 50 | "test:all": [ 51 | "@cs:check", 52 | "@sniffer:check", 53 | "@stan", 54 | "@test" 55 | ], 56 | "test:coverage": "php -d xdebug.mode=coverage -r \"require 'vendor/bin/phpunit';\" -- --configuration phpunit.xml --do-not-cache-result --colors=always --coverage-clover build/logs/clover.xml --coverage-html build/coverage" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Engine/EngineInterface.php: -------------------------------------------------------------------------------- 1 | getRealPath(); 26 | 27 | if ($realPath === false) { 28 | throw new RuntimeException(sprintf('File not found: %s', $file->getFilename())); 29 | } 30 | 31 | if (!$this->isPng($file)) { 32 | // This is not a PNG 33 | return new BombScannerResult(false); 34 | } 35 | 36 | $file->rewind(); 37 | $file->fread(8 + 4); 38 | $idr = $file->fread(4); 39 | 40 | // Make sure we have an IHDR 41 | if ($idr !== 'IHDR') { 42 | throw new RuntimeException('No PNG IHDR header found, invalid PNG file.'); 43 | } 44 | 45 | // PNG actually stores Width and height integers in big-endian. 46 | $width = unpack('N', (string)$file->fread(4))[1]; 47 | $height = unpack('N', (string)$file->fread(4))[1]; 48 | 49 | if ($width > 10000 || $height > 10000) { 50 | // Invalid image 51 | return new BombScannerResult(true); 52 | } 53 | 54 | return new BombScannerResult(false); 55 | } 56 | 57 | /** 58 | * Detect file type. 59 | * 60 | * @param SplFileObject $file The file 61 | * 62 | * @return bool The status 63 | */ 64 | private function isPng(SplFileObject $file): bool 65 | { 66 | $file->rewind(); 67 | 68 | return $file->fread(4) === chr(0x89) . 'PNG'; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Engine/RarBombEngine.php: -------------------------------------------------------------------------------- 1 | maxRatio = $maxRatio; 28 | } 29 | 30 | /** 31 | * Scan for RAR bomb. 32 | * 33 | * @param SplFileObject $file The rar file 34 | * 35 | * @throws RuntimeException 36 | * 37 | * @return BombScannerResult The result 38 | */ 39 | public function scanFile(SplFileObject $file): BombScannerResult 40 | { 41 | $realPath = $file->getRealPath(); 42 | 43 | if ($realPath === false) { 44 | throw new RuntimeException(sprintf('File not found: %s', $file->getFilename())); 45 | } 46 | 47 | if (!$this->isRar($file)) { 48 | return new BombScannerResult(false); 49 | } 50 | 51 | $fileReader = new RarFileReader(); 52 | $rarArchive = $fileReader->openFile($file); 53 | 54 | $ration = 0; 55 | 56 | // http://web.archive.org/web/20201223151701/https://aerasec.de/security/advisories/decompression-bomb-vulnerability.html 57 | foreach ($rarArchive->getEntries() as $entry) { 58 | $compressedSize = $entry->getPackedSize(); 59 | $originalSize = (float)$entry->getUnpackedSize(); 60 | $ration = $originalSize / $compressedSize; 61 | break; 62 | } 63 | 64 | return new BombScannerResult($ration >= $this->maxRatio); 65 | } 66 | 67 | /** 68 | * Detect file type. 69 | * 70 | * @param SplFileObject $file The file 71 | * 72 | * @return bool The status 73 | */ 74 | private function isRar(SplFileObject $file): bool 75 | { 76 | $file->rewind(); 77 | 78 | return $file->fread(3) === "\x52\x61\x72"; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Engine/ZipBombEngine.php: -------------------------------------------------------------------------------- 1 | getRealPath(); 27 | 28 | if ($realPath === false) { 29 | throw new RuntimeException(sprintf('File not found: %s', $file->getFilename())); 30 | } 31 | 32 | if (!$this->isZip($file)) { 33 | return new BombScannerResult(false); 34 | } 35 | 36 | $zip = $this->openZip($realPath); 37 | 38 | // Sum ZIP index file size 39 | $i = 0; 40 | $size = 0; 41 | while ($idx = $zip->statIndex($i++)) { 42 | $size += $idx['size']; 43 | } 44 | 45 | // Reading the ZIP header 46 | // https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers 47 | // Offset 22, 4 bytes: Uncompressed size 48 | $file->rewind(); 49 | $file->fread(22); 50 | 51 | // Convert 4 bytes, little-endian to int 52 | $size2 = unpack('V', (string)$file->fread(4))[1]; 53 | 54 | // Header uncompressed size must be the same as files uncompressed size 55 | $result = $size !== $size2; 56 | 57 | return new BombScannerResult($result); 58 | } 59 | 60 | /** 61 | * Open zip file. 62 | * 63 | * @param string $filename The zip file 64 | * 65 | * @throws RuntimeException 66 | * 67 | * @return ZipArchive The zip archive 68 | */ 69 | private function openZip(string $filename): ZipArchive 70 | { 71 | $zip = new ZipArchive(); 72 | $result = $zip->open($filename, ZIPARCHIVE::CREATE); 73 | 74 | if ($result !== true) { 75 | $errorMap = [ 76 | ZipArchive::ER_EXISTS => 'File already exists', 77 | ZipArchive::ER_INCONS => 'Zip archive inconsistent.', 78 | ZipArchive::ER_INVAL => 'Invalid argument.', 79 | ZipArchive::ER_MEMORY => 'Malloc failure.', 80 | ZipArchive::ER_NOENT => 'No such file.', 81 | ZipArchive::ER_NOZIP => 'Not a zip archive.', 82 | ZipArchive::ER_OPEN => 'Can\'t open file.', 83 | ZipArchive::ER_READ => 'Read error.', 84 | ZipArchive::ER_SEEK => 'Seek error.', 85 | ]; 86 | 87 | $errorReason = $errorMap[(int)$result] ?? 'Unknown error.'; 88 | 89 | throw new RuntimeException(sprintf('Unable to open: %s, reason: %s', $filename, $errorReason)); 90 | } 91 | 92 | return $zip; 93 | } 94 | 95 | /** 96 | * Detect file type. 97 | * 98 | * @param SplFileObject $file The file 99 | * 100 | * @return bool The status 101 | */ 102 | private function isZip(SplFileObject $file): bool 103 | { 104 | $file->rewind(); 105 | 106 | return $file->fread(4) === "PK\3\4"; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Scanner/BombScanner.php: -------------------------------------------------------------------------------- 1 | engines[] = $engine; 26 | } 27 | 28 | /** 29 | * Scan archive file. 30 | * 31 | * @param SplFileObject $file The archive file 32 | * 33 | * @return BombScannerResult The scanning result 34 | */ 35 | public function scanFile(SplFileObject $file): BombScannerResult 36 | { 37 | foreach ($this->engines as $engines) { 38 | $result = $engines->scanFile($file); 39 | 40 | if ($result->isBomb()) { 41 | return $result; 42 | } 43 | } 44 | 45 | return new BombScannerResult(false); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Scanner/BombScannerResult.php: -------------------------------------------------------------------------------- 1 | isBomb = $isBomb; 23 | } 24 | 25 | /** 26 | * Get result. 27 | * 28 | * @return bool The result 29 | */ 30 | public function isBomb(): bool 31 | { 32 | return $this->isBomb; 33 | } 34 | 35 | /** 36 | * Compare with other value object. 37 | * 38 | * @param BombScannerResult $other The other type 39 | * 40 | * @return bool Status 41 | */ 42 | public function equals(BombScannerResult $other): bool 43 | { 44 | return $this->isBomb === $other->isBomb; 45 | } 46 | } 47 | --------------------------------------------------------------------------------