Merge pull request #13974 from eileenmcnaughton/array_format7
[civicrm-core.git] / CRM / Core / IDS.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2019
32 */
33 class CRM_Core_IDS {
34
35 /**
36 * Define the threshold for the ids reactions.
37 */
38 private $threshold = [
39 'log' => 25,
40 'warn' => 50,
41 'kick' => 75,
42 ];
43
44 /**
45 * @var string
46 */
47 private $path;
48
49 /**
50 * Check function.
51 *
52 * This function includes the IDS vendor parts and runs the
53 * detection routines on the request array.
54 *
55 * @param array $route
56 *
57 * @return bool
58 */
59 public function check($route) {
60 if (CRM_Core_Permission::check('skip IDS check')) {
61 return NULL;
62 }
63
64 // lets bypass a few civicrm urls from this check
65 $skip = ['civicrm/admin/setting/updateConfigBackend', 'civicrm/admin/messageTemplates'];
66 CRM_Utils_Hook::idsException($skip);
67 $this->path = $route['path'];
68 if (in_array($this->path, $skip)) {
69 return NULL;
70 }
71
72 $init = self::create(self::createRouteConfig($route));
73
74 // Add request url and user agent.
75 $_REQUEST['IDS_request_uri'] = $_SERVER['REQUEST_URI'];
76 if (isset($_SERVER['HTTP_USER_AGENT'])) {
77 $_REQUEST['IDS_user_agent'] = $_SERVER['HTTP_USER_AGENT'];
78 }
79
80 require_once 'IDS/Monitor.php';
81 $ids = new \IDS_Monitor($_REQUEST, $init);
82
83 $result = $ids->run();
84 if (!$result->isEmpty()) {
85 $this->react($result);
86 }
87
88 return TRUE;
89 }
90
91 /**
92 * Create a new PHPIDS configuration object.
93 *
94 * @param array $config
95 * PHPIDS configuration array (per INI format).
96 * @return \IDS_Init
97 */
98 protected static function create($config) {
99 require_once 'IDS/Init.php';
100 $init = \IDS_Init::init(NULL);
101 $init->setConfig($config, TRUE);
102
103 // Cleanup
104 $reflection = new \ReflectionProperty('IDS_Init', 'instances');
105 $reflection->setAccessible(TRUE);
106 $value = $reflection->getValue(NULL);
107 unset($value[NULL]);
108 $reflection->setValue(NULL, $value);
109
110 return $init;
111 }
112
113 /**
114 * Create conservative, minimalist IDS configuration.
115 *
116 * @return array
117 */
118 public static function createBaseConfig() {
119 $config = \CRM_Core_Config::singleton();
120 $tmpDir = empty($config->uploadDir) ? CIVICRM_TEMPLATE_COMPILEDIR : $config->uploadDir;
121 global $civicrm_root;
122
123 return [
124 'General' => [
125 'filter_type' => 'xml',
126 'filter_path' => "{$civicrm_root}/packages/IDS/default_filter.xml",
127 'tmp_path' => $tmpDir,
128 'HTML_Purifier_Path' => $civicrm_root . '/vendor/ezyang/htmlpurifier/library/HTMLPurifier.auto.php',
129 'HTML_Purifier_Cache' => $tmpDir,
130 'scan_keys' => '',
131 'exceptions' => ['__utmz', '__utmc'],
132 ],
133 ];
134 }
135
136 /**
137 * Create the standard, general-purpose IDS configuration used by many pages.
138 *
139 * @return array
140 */
141 public static function createStandardConfig() {
142 $excs = [
143 'widget_code',
144 'html_message',
145 'text_message',
146 'body_html',
147 'msg_html',
148 'msg_text',
149 'msg_subject',
150 'description',
151 'intro',
152 'thankyou_text',
153 'intro_text',
154 'body_text',
155 'footer_text',
156 'thankyou_text',
157 'tf_thankyou_text',
158 'thankyou_footer',
159 'thankyou_footer_text',
160 'new_text',
161 'renewal_text',
162 'help_pre',
163 'help_post',
164 'confirm_title',
165 'confirm_text',
166 'confirm_footer_text',
167 'confirm_email_text',
168 'report_header',
169 'report_footer',
170 'data',
171 'json',
172 'instructions',
173 'suggested_message',
174 'page_text',
175 'details',
176 ];
177
178 $result = self::createBaseConfig();
179
180 $result['General']['exceptions'] = array_merge(
181 $result['General']['exceptions'],
182 $excs
183 );
184
185 return $result;
186 }
187
188 /**
189 * @param array $route
190 * @return array
191 */
192 public static function createRouteConfig($route) {
193 $config = \CRM_Core_IDS::createStandardConfig();
194 foreach (['json', 'html', 'exceptions'] as $section) {
195 if (isset($route['ids_arguments'][$section])) {
196 if (!isset($config['General'][$section])) {
197 $config['General'][$section] = [];
198 }
199 foreach ($route['ids_arguments'][$section] as $v) {
200 $config['General'][$section][] = $v;
201 }
202 $config['General'][$section] = array_unique($config['General'][$section]);
203 }
204 }
205 return $config;
206 }
207
208 /**
209 * This function reacts on the values in the incoming results array.
210 *
211 * Depending on the impact value certain actions are
212 * performed.
213 *
214 * @param IDS_Report $result
215 *
216 * @return bool
217 */
218 public function react(IDS_Report $result) {
219
220 $impact = $result->getImpact();
221 if ($impact >= $this->threshold['kick']) {
222 $this->log($result, 3, $impact);
223 $this->kick();
224 return TRUE;
225 }
226 elseif ($impact >= $this->threshold['warn']) {
227 $this->log($result, 2, $impact);
228 $this->warn($result);
229 return TRUE;
230 }
231 elseif ($impact >= $this->threshold['log']) {
232 $this->log($result, 0, $impact);
233 return TRUE;
234 }
235 else {
236 return TRUE;
237 }
238 }
239
240 /**
241 * This function writes an entry about the intrusion to the database.
242 *
243 * @param array $result
244 * @param int $reaction
245 *
246 * @return bool
247 */
248 private function log($result, $reaction = 0) {
249 $ip = (isset($_SERVER['SERVER_ADDR']) &&
250 $_SERVER['SERVER_ADDR'] != '127.0.0.1') ? $_SERVER['SERVER_ADDR'] : (
251 isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : '127.0.0.1'
252 );
253
254 $data = [];
255 $session = CRM_Core_Session::singleton();
256 foreach ($result as $event) {
257 $data[] = [
258 'name' => $event->getName(),
259 'value' => stripslashes($event->getValue()),
260 'page' => $_SERVER['REQUEST_URI'],
261 'userid' => $session->get('userID'),
262 'session' => session_id() ? session_id() : '0',
263 'ip' => $ip,
264 'reaction' => $reaction,
265 'impact' => $result->getImpact(),
266 ];
267 }
268
269 CRM_Core_Error::debug_var('IDS Detector Details', $data);
270 return TRUE;
271 }
272
273 /**
274 * Warn about IDS.
275 *
276 * @param array $result
277 *
278 * @return array
279 */
280 private function warn($result) {
281 return $result;
282 }
283
284 /**
285 * Create an error that prevents the user from continuing.
286 *
287 * @throws \Exception
288 */
289 private function kick() {
290 $session = CRM_Core_Session::singleton();
291 $session->reset(2);
292
293 $msg = ts('There is a validation error with your HTML input. Your activity is a bit suspicious, hence aborting');
294
295 if (in_array(
296 $this->path,
297 ["civicrm/ajax/rest", "civicrm/api/json"]
298 )) {
299 require_once "api/v3/utils.php";
300 $error = civicrm_api3_create_error(
301 $msg,
302 [
303 'IP' => $_SERVER['REMOTE_ADDR'],
304 'error_code' => 'IDS_KICK',
305 'level' => 'security',
306 'referer' => $_SERVER['HTTP_REFERER'],
307 'reason' => 'XSS suspected',
308 ]
309 );
310 CRM_Utils_JSON::output($error);
311 }
312 CRM_Core_Error::fatal($msg);
313 }
314
315 }