1: <?php
2:
3: /*****************************************************************************************
4: * X2Engine Open Source Edition is a customer relationship management program developed by
5: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
6: *
7: * This program is free software; you can redistribute it and/or modify it under
8: * the terms of the GNU Affero General Public License version 3 as published by the
9: * Free Software Foundation with the addition of the following permission added
10: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
12: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
13: *
14: * This program is distributed in the hope that it will be useful, but WITHOUT
15: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
17: * details.
18: *
19: * You should have received a copy of the GNU Affero General Public License along with
20: * this program; if not, see http://www.gnu.org/licenses or write to the Free
21: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22: * 02110-1301 USA.
23: *
24: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
25: * California 95067, USA. or at email address [email protected].
26: *
27: * The interactive user interfaces in modified source and object code versions
28: * of this program must display Appropriate Legal Notices, as required under
29: * Section 5 of the GNU Affero General Public License version 3.
30: *
31: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32: * these Appropriate Legal Notices must retain the display of the "Powered by
33: * X2Engine" logo. If the display of the logo is not reasonably feasible for
34: * technical reasons, the Appropriate Legal Notices must display the words
35: * "Powered by X2Engine".
36: *****************************************************************************************/
37:
38: /**
39: * Checks uploaded files in the web request for invalid extensions.
40: *
41: * Intended as a catch-all for attempted arbitrary file type uploads.
42: *
43: * @package application.components.filters
44: * @author Demitri Morgan <[email protected]>
45: */
46: class FileUploadsFilter extends CFilter {
47:
48: /**
49: * Regular expression for blacklisted files.
50: *
51: * Does not match end of string to prevent circumvention via the methods
52: * described in OWASP's Unrestricted File Upload article:
53: *
54: * https://www.owasp.org/index.php/Unrestricted_File_Upload#Using_Black-List_for_Files.E2.80.99_Extensions
55: */
56: const EXT_BLACKLIST = '/\.\s*(?P<ext>html|htm|js|jsb|mhtml|mht|xhtml|xht|php|pht|phtml|php3|php4|php5|phps|shtml|jhtml|pl|py|cgi|exe|scr|dll|msi|vbs|bat|com|pif|cmd|vxd|cpl|ini|conf|cnf|key|iv|htaccess)\b/i';
57:
58: /**
59: * List of mime-types that uploaded files should never have
60: * @var type
61: */
62: private $_mimeBlacklist = array(
63: 'text/html', 'text/javascript', 'text/x-javascript',
64: 'application/x-shellscript', 'application/x-php', 'text/x-php',
65: 'text/x-python', 'text/x-perl', 'text/x-bash', 'text/x-sh',
66: 'text/x-csh', 'text/scriptlet', 'application/x-msdownload',
67: 'application/x-msmetafile'
68: );
69:
70: /**
71: * Returns true if the file is safe to upload.
72: *
73: * Will use fileinfo if available for determining mime type of the uploaded file.
74: * @param array $file
75: */
76: public function checkFilename($filename){
77: if(preg_match(self::EXT_BLACKLIST, $filename,$match)){
78: AuxLib::debugLog('Throwing exception for array: '.var_export($_FILES,1));
79: throw new CHttpException(403,Yii::t('app','Forbidden file type: {ext}',array('{ext}'=>$match['ext'])));
80: }
81: }
82:
83: public function checkFiles(array $inputs){
84:
85: foreach($inputs as $fieldName => $input){
86: // Structure:
87: // [field name] =>
88: // 'name' => [name(s)]
89: // 'type' => [type(s)]
90: // 'tmp_name' => [name(s)]
91: // 'error' => [error(s)]
92: // 'size' => [size(s)]
93: if(!isset($input['name'])){
94: throw new CHttpException(400, Yii::t('app', 'Uploaded files must have names.'));
95: }elseif(is_array($input['name'])){
96: // Multiple files in this input field
97: foreach($input['name'] as $name){
98: if (is_array ($name)) {
99: // nesting can go one level deeper if file is being uploaded as
100: // <model name>["<attribute name>"][]
101:
102: $names = $name;
103: foreach ($names as $name) {
104: $this->checkFilename($name);
105: }
106: } else {
107: $this->checkFilename($name);
108: }
109: }
110: if((bool) ($finfo = FileUtil::finfo())) {
111: $types = array();
112: foreach ($input['tmp_name'] as $path) {
113: if (is_array ($path)) {
114: $paths = $path;
115: foreach ($paths as $path) {
116: if(file_exists($path)) {
117: $types[] = finfo_file($finfo, $path, FILEINFO_MIME);
118: }
119: }
120: } else {
121: if(file_exists($path)) {
122: $types[] = finfo_file($finfo, $path, FILEINFO_MIME);
123: }
124: }
125: }
126: } else {
127: $types = $input['type'];
128: }
129: if($forbidden = count(array_intersect($types, $this->_mimeBlacklist)) > 0){
130: throw new CHttpException(403, Yii::t('app', 'List of uploaded files includes forbidden MIME types: {types}', array('{types}' => implode(',', $forbidden))));
131: }
132: }else{
133: // One file in this input field
134: $this->checkFilename($input['name']);
135: if(file_exists($input['tmp_name']) && (bool) ($finfo = FileUtil::finfo())) {
136: $type = finfo_file($finfo, $input['tmp_name'], FILEINFO_MIME);
137: } else {
138: $type = $input['type'];
139: }
140: if(in_array($type,$this->_mimeBlacklist)) {
141: throw new CHttpException(403, Yii::t('app','Forbidden MIME type for file: {file}',array('{file}'=>$input['name'])));
142: }
143: }
144: }
145: }
146:
147: protected function preFilter($filterChain){
148: if(empty($_FILES)){ // No files to be uploaded
149: return true;
150: }
151:
152: $this->checkFiles($_FILES);
153: return true;
154: }
155:
156: }
157:
158: ?>
159: