CRM-19179: Only allow one 'set primary' to be checked per entity
[civicrm-core.git] / CRM / Core / IDS.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
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-2016
32 */
33 class CRM_Core_IDS {
34
35 /**
36 * Define the threshold for the ids reactions.
37 */
38 private $threshold = array(
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 $args
56 * List of path parts.
57 *
58 * @return bool
59 */
60 public function check($args) {
61 // lets bypass a few civicrm urls from this check
62 $skip = array('civicrm/admin/setting/updateConfigBackend', 'civicrm/admin/messageTemplates');
63 CRM_Utils_Hook::idsException($skip);
64 $this->path = implode('/', $args);
65 if (in_array($this->path, $skip)) {
66 return NULL;
67 }
68
69 // Add request url and user agent.
70 $_REQUEST['IDS_request_uri'] = $_SERVER['REQUEST_URI'];
71 if (isset($_SERVER['HTTP_USER_AGENT'])) {
72 $_REQUEST['IDS_user_agent'] = $_SERVER['HTTP_USER_AGENT'];
73 }
74
75 $configFile = self::createConfigFile(FALSE);
76
77 // init the PHPIDS and pass the REQUEST array
78 require_once 'IDS/Init.php';
79 try {
80 $init = IDS_Init::init($configFile);
81 $ids = new IDS_Monitor($_REQUEST, $init);
82 }
83 catch (Exception $e) {
84 // might be an old stale copy of Config.IDS.ini
85 // lets try to rebuild it again and see if it works
86 $configFile = self::createConfigFile(TRUE);
87 $init = IDS_Init::init($configFile);
88 $ids = new IDS_Monitor($_REQUEST, $init);
89 }
90
91 $result = $ids->run();
92 if (!$result->isEmpty()) {
93 $this->react($result);
94 }
95
96 return TRUE;
97 }
98
99 /**
100 * Create the default config file for the IDS system.
101 *
102 * @param bool $force
103 * Should we recreate it irrespective if it exists or not.
104 *
105 * @return string
106 * the full path to the config file
107 */
108 public static function createConfigFile($force = FALSE) {
109 $config = CRM_Core_Config::singleton();
110 $configFile = $config->configAndLogDir . 'Config.IDS.ini';
111 if (!$force && file_exists($configFile)) {
112 return $configFile;
113 }
114
115 $tmpDir = empty($config->uploadDir) ? CIVICRM_TEMPLATE_COMPILEDIR : $config->uploadDir;
116
117 // also clear the stat cache in case we are upgrading
118 clearstatcache();
119
120 global $civicrm_root;
121 $contents = "
122 [General]
123 filter_type = xml
124 filter_path = {$civicrm_root}/packages/IDS/default_filter.xml
125 tmp_path = $tmpDir
126 HTML_Purifier_Path = IDS/vendors/htmlpurifier/HTMLPurifier.auto.php
127 HTML_Purifier_Cache = $tmpDir
128 scan_keys = false
129 exceptions[] = __utmz
130 exceptions[] = __utmc
131 exceptions[] = widget_code
132 exceptions[] = html_message
133 exceptions[] = text_message
134 exceptions[] = body_html
135 exceptions[] = msg_html
136 exceptions[] = msg_text
137 exceptions[] = msg_subject
138 exceptions[] = description
139 exceptions[] = intro
140 exceptions[] = thankyou_text
141 exceptions[] = intro_text
142 exceptions[] = body_text
143 exceptions[] = footer_text
144 exceptions[] = thankyou_text
145 exceptions[] = tf_thankyou_text
146 exceptions[] = thankyou_footer
147 exceptions[] = thankyou_footer_text
148 exceptions[] = new_text
149 exceptions[] = renewal_text
150 exceptions[] = help_pre
151 exceptions[] = help_post
152 exceptions[] = confirm_title
153 exceptions[] = confirm_text
154 exceptions[] = confirm_footer_text
155 exceptions[] = confirm_email_text
156 exceptions[] = report_header
157 exceptions[] = report_footer
158 exceptions[] = data
159 exceptions[] = json
160 exceptions[] = instructions
161 exceptions[] = suggested_message
162 exceptions[] = page_text
163 ";
164 if (file_put_contents($configFile, $contents) === FALSE) {
165 CRM_Core_Error::movedSiteError($configFile);
166 }
167
168 // also create the .htaccess file so we prevent the reading of the log and ini files
169 // via a browser, CRM-3875
170 CRM_Utils_File::restrictAccess($config->configAndLogDir);
171
172 return $configFile;
173 }
174
175 /**
176 * This function reacts on the values in the incoming results array.
177 *
178 * Depending on the impact value certain actions are
179 * performed.
180 *
181 * @param IDS_Report $result
182 *
183 * @return bool
184 */
185 private function react(IDS_Report $result) {
186
187 $impact = $result->getImpact();
188 if ($impact >= $this->threshold['kick']) {
189 $this->log($result, 3, $impact);
190 $this->kick();
191 return TRUE;
192 }
193 elseif ($impact >= $this->threshold['warn']) {
194 $this->log($result, 2, $impact);
195 $this->warn($result);
196 return TRUE;
197 }
198 elseif ($impact >= $this->threshold['log']) {
199 $this->log($result, 0, $impact);
200 return TRUE;
201 }
202 else {
203 return TRUE;
204 }
205 }
206
207 /**
208 * This function writes an entry about the intrusion to the database.
209 *
210 * @param array $result
211 * @param int $reaction
212 *
213 * @return bool
214 */
215 private function log($result, $reaction = 0) {
216 $ip = (isset($_SERVER['SERVER_ADDR']) &&
217 $_SERVER['SERVER_ADDR'] != '127.0.0.1') ? $_SERVER['SERVER_ADDR'] : (
218 isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : '127.0.0.1'
219 );
220
221 $data = array();
222 $session = CRM_Core_Session::singleton();
223 foreach ($result as $event) {
224 $data[] = array(
225 'name' => $event->getName(),
226 'value' => stripslashes($event->getValue()),
227 'page' => $_SERVER['REQUEST_URI'],
228 'userid' => $session->get('userID'),
229 'session' => session_id() ? session_id() : '0',
230 'ip' => $ip,
231 'reaction' => $reaction,
232 'impact' => $result->getImpact(),
233 );
234 }
235
236 CRM_Core_Error::debug_var('IDS Detector Details', $data);
237 return TRUE;
238 }
239
240 /**
241 * Warn about IDS.
242 *
243 * @param array $result
244 *
245 * @return array
246 */
247 private function warn($result) {
248 return $result;
249 }
250
251 /**
252 * Create an error that prevents the user from continuing.
253 *
254 * @throws \Exception
255 */
256 private function kick() {
257 $session = CRM_Core_Session::singleton();
258 $session->reset(2);
259
260 $msg = ts('There is a validation error with your HTML input. Your activity is a bit suspicious, hence aborting');
261
262 if (in_array(
263 $this->path,
264 array("civicrm/ajax/rest", "civicrm/api/json")
265 )) {
266 require_once "api/v3/utils.php";
267 $error = civicrm_api3_create_error(
268 $msg,
269 array(
270 'IP' => $_SERVER['REMOTE_ADDR'],
271 'error_code' => 'IDS_KICK',
272 'level' => 'security',
273 'referer' => $_SERVER['HTTP_REFERER'],
274 'reason' => 'XSS suspected',
275 )
276 );
277 CRM_Utils_JSON::output($error);
278 }
279 CRM_Core_Error::fatal($msg);
280 }
281
282 }