removeDirectory - prevent accidental whipe of, for example, root dir

Is there a best practice preventing BaseFileHelper::removeDirectory() from recursive deleting directory / ? Or a ‘too high’ directory in the domain structure?
When having bad luck a script that pulls this off manages to whipe the complete domain directory from the filesystem (I had bad luck a few times this year).
Besides, it’s almost undoable tracking the culprit (files removed by php are ‘legit’ actions), but found it in the end!

Some ideas of mine:

write a wrapper, and check:

  • $dir is not equal to ‘/’, or
  • $dir is minimum x levels away from DOCUMENT_ROOT, or
  • $dir is in whitelist of deletable dirs

How did others solve this problem?

Yii2: baseFileHelper::removeDirectory
Yii1: CFileHelper::removeDirectory

You could wrap it into another helper that does necessary checks and throws an exception.

Okay,

I’ve created a ‘whitelist’ of dirs that may be cleared from subdirs,
and wrapped created a removeDirectory() wrapper to solve it:

Config:

'params' => array(
	/* A whitelist that's used to prevent functions as:
	 *  - CFileHelper::removeDirectory()
	 * to completely whipe the domain folder, subdomain or just an important folder.
	 * The subdirectories of the following paths are considered 'save' for recursive removal
	 * Example: "/tmp" it's save to remove recursive "/tmp/test" (but not /tmp itself!)
	 * WARNING: Make sure you don't include a path like '/' or 'document root' ! ! !
	 */			
	'directories_safe_to_empty' => array(
		'/tmp',
		$documentRoot.'/runtime/cache',
		$documentRoot.'/httpdocs/assets',
	)
)

Snippet from my FileHelper using above whitelist:

class FileHelper extends CFileHelper
{
	/*
	 * Removes directory after confirming it's whitelisted as 'safe for recursive removal'.
	 * See whitelist in config['directories_safe_to_empty']
	 */
	public static function removeDirectory($directory, $options=array())
	{
		$realPath = realpath($directory);
		if(!$realPath){
			Yii::log("Invalid path passed to removerecursive according to realpath: '$directory'", "warning", 'shared.components.FileHelper');
			// path doesnt exist so it doesnt have to be removed :)
			return true;
		}

		if(FileHelper::isSafeForRecursiveRemoval($realPath)===false){
			return false;
		}
		
		CFileHelper::removeDirectory($realPath, $options);
	}
	
	/*
	 * Checks the passed dir against a whitelist of dirs that are safe for recursvie removal.
	 * The passed dir must be a subdir of one of the whitelisted dirs !
	 * @param string $dir the path of a dir to be checked
	 * returns boolean
	 */
	public static function isSafeForRecursiveRemoval($dir)
	{
		$realPath = realpath($dir);
		if(!$realPath)
			return false;
		
		// Yii::app()->params['paths']
		$config = Yii::app()->params;
		if(!isset($config['directories_safe_to_empty']) || !is_array($config['directories_safe_to_empty'])){
			Yii::log("Config key params['directories_safe_to_empty'] not set or not an array", "error", 'shared.components.FileHelper');
			return false;
		}
		
		$continueRemoval = false;
		foreach($config['directories_safe_to_empty'] as $idx => $safeParent){
			$realParent = realpath($safeParent);
			if(!$realParent){	
				Yii::log("Invalid path according to realpath: params['directories_safe_to_empty'][\$idx] = '$safeParent'", "error", 'shared.components.FileHelper');
				continue;
			}
			
			// dir to be removed is in parent's path,
			// and path of dir to be removed is longer than parents
			if(strpos($realPath, $realParent.'/') === 0 && (strlen($realPath) > strlen($realParent.'/'))){
				return true;
			}
		}
		
		// echo 'path is not safe for recursive removal';
		Yii::log("Trying to recursive remove unsafe dir: '$realPath'", "warning", 'shared.components.FileHelper');
		// Yii::trace("Trying to recursive remove unsafe dir: '$realPath'", 'shared.components.FileHelper');
		return false;
	}
}
1 Like