Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"doctrine/instantiator": "^1.3",
"friendsofphp/php-cs-fixer": "^3.88",
"marcocesarato/php-conventional-changelog": "^1.17",
"phpunit/phpunit": "^8.5|^9.5",
"phpunit/phpunit": "^8.5.52|^9.6.33",
"psr/log": "^1.1",
"symfony/console": "^5.4",
"symfony/deprecation-contracts": "^2.5",
Expand Down
54 changes: 27 additions & 27 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion src/Actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,15 @@ public static function addToWhitelist($file, $patternFound)
'match' => $match,
];
}

$encoded = json_encode($whitelist, JSON_INVALID_UTF8_SUBSTITUTE);
if ($encoded === false) {
return false;
}

Scanner::setWhitelist($whitelist);

return file_put_contents(Scanner::getPathWhitelist(), json_encode($whitelist));
return file_put_contents(Scanner::getPathWhitelist(), $encoded);
}

/**
Expand Down
117 changes: 117 additions & 0 deletions tests/Unit/ActionsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

/**
* PHP Antimalware Scanner.
*
* @author Marco Cesarato <cesarato.developer@gmail.com>
* @license http://opensource.org/licenses/gpl-3.0.html GNU Public License
*
* @see https://github.com/marcocesarato/PHP-Antimalware-Scanner
*/

namespace AMWScan\Tests\Unit;

use AMWScan\Actions;
use AMWScan\Scanner;
use PHPUnit\Framework\TestCase;

class ActionsTest extends TestCase
{
private $whitelistFile;

protected function setUp(): void
{
$this->whitelistFile = tempnam(sys_get_temp_dir(), 'whitelist_') . '.json';
Scanner::$whitelist = [];
Scanner::$pathWhitelist = $this->whitelistFile;
Scanner::$pathScan = '/scan/';
}

protected function tearDown(): void
{
if (file_exists($this->whitelistFile)) {
unlink($this->whitelistFile);
}
Scanner::$whitelist = [];
}

public function testAddToWhitelistWritesValidJson()
{
$patternFound = [
[
'key' => 'eval_exploit',
'line' => 5,
'match' => 'eval($code)',
],
];

$result = Actions::addToWhitelist('/scan/malware.php', $patternFound);

$this->assertNotFalse($result, 'addToWhitelist should return bytes written on success');
$this->assertFileExists($this->whitelistFile);

$contents = file_get_contents($this->whitelistFile);
$decoded = json_decode($contents, true);
$this->assertIsArray($decoded, 'Whitelist file should contain valid JSON');
$this->assertCount(1, $decoded);
}

public function testAddToWhitelistPreservesExistingEntries()
{
$firstPattern = [
['key' => 'eval_exploit', 'line' => 5, 'match' => 'eval($code)'],
];
Actions::addToWhitelist('/scan/first.php', $firstPattern);

$secondPattern = [
['key' => 'base64_exploit', 'line' => 10, 'match' => 'base64_decode($data)'],
];
Actions::addToWhitelist('/scan/second.php', $secondPattern);

$contents = file_get_contents($this->whitelistFile);
$decoded = json_decode($contents, true);
$this->assertIsArray($decoded);
$this->assertCount(2, $decoded, 'Both entries should be preserved in the whitelist');
}

public function testAddToWhitelistWithInvalidUtf8DoesNotTruncateFile()
{
// Write initial valid content to the whitelist file
$initialContent = json_encode(['existing_key' => ['file' => '/clean.php', 'exploit' => 'test', 'line' => 1, 'match' => 'safe']]);
file_put_contents($this->whitelistFile, $initialContent);
Scanner::$whitelist = json_decode($initialContent, true);

// Attempt to add an entry whose 'match' contains invalid UTF-8 binary data
$patternFound = [
[
'key' => 'binary_exploit',
'line' => 3,
'match' => "evil\x80\x81\x82code", // invalid UTF-8 bytes
],
];

Actions::addToWhitelist('/scan/malware.php', $patternFound);

// The whitelist file must NOT be truncated to 0 bytes
$this->assertGreaterThan(0, filesize($this->whitelistFile), 'Whitelist file must not be truncated when a match contains invalid UTF-8');
}

public function testAddToWhitelistWithInvalidUtf8UsesSubstitution()
{
$patternFound = [
[
'key' => 'binary_exploit',
'line' => 3,
'match' => "evil\x80\x81code", // invalid UTF-8 bytes
],
];

$result = Actions::addToWhitelist('/scan/malware.php', $patternFound);

// With JSON_INVALID_UTF8_SUBSTITUTE the encoding succeeds, so bytes are written
$this->assertNotFalse($result, 'addToWhitelist should succeed by substituting invalid UTF-8 characters');
$contents = file_get_contents($this->whitelistFile);
$decoded = json_decode($contents, true);
$this->assertIsArray($decoded, 'Result should be a valid JSON array even with substituted characters');
}
}
Loading