dolibarr 23.0.3
waf.inc.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2004-2025 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2025 Frédéric France <frederic.france@free.fr>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
27// To disable the WAF for GET and POST and PHP_SELF, uncomment this
28//define('NOSCANPHPSELFFORINJECTION', 1);
29//define('NOSCANGETFORINJECTION', 1);
30//define('NOSCANPOSTFORINJECTION', 1 or array('param1', 'param2'...));
31//define('NOSCANAUDIOFORINJECTION', 1);
32//define('NOSCANIFRAMEFORINJECTION', 1);
33//define('NOSCANOBJECTFORINJECTION', 1);
34
35
43{
44 $arrayofcommonemoji = array(
45 'misc' => array('2600', '26FF'), // Miscellaneous Symbols
46 'ding' => array('2700', '27BF'), // Dingbats
47 '????' => array('9989', '9989'), // Variation Selectors
48 'vars' => array('FE00', 'FE0F'), // Variation Selectors
49 'pict' => array('1F300', '1F5FF'), // Miscellaneous Symbols and Pictographs
50 'emot' => array('1F600', '1F64F'), // Emoticons
51 'tran' => array('1F680', '1F6FF'), // Transport and Map Symbols
52 'flag' => array('1F1E0', '1F1FF'), // Flags (note: may be 1F1E6 instead of 1F1E0)
53 'supp' => array('1F900', '1F9FF'), // Supplemental Symbols and Pictographs
54 );
55
56 return $arrayofcommonemoji;
57}
58
67{
68 $newstringnumentity = preg_replace('/;$/', '', $matches[1]);
69 //print ' $newstringnumentity='.$newstringnumentity;
70
71 if (preg_match('/^x/i', $newstringnumentity)) { // if numeric is hexadecimal
72 $newstringnumentity = hexdec(preg_replace('/^x/i', '', $newstringnumentity));
73 } else {
74 $newstringnumentity = (int) $newstringnumentity;
75 }
76
77 // The numeric values we don't want as entities because they encode ascii char, and why using html entities on ascii except for hacking ?
78 if (($newstringnumentity >= 47 && $newstringnumentity <= 59) || ($newstringnumentity >= 65 && $newstringnumentity <= 90) || ($newstringnumentity >= 97 && $newstringnumentity <= 122)) {
79 return chr((int) $newstringnumentity);
80 }
81
82 // The numeric values we want in UTF8 instead of entities because it is emoji
83 $arrayofemojis = getArrayOfEmoji();
84 foreach ($arrayofemojis as $valarray) {
85 if ($newstringnumentity >= hexdec($valarray[0]) && $newstringnumentity <= hexdec($valarray[1])) {
86 // This is a known emoji
87 return html_entity_decode($matches[0], ENT_COMPAT | ENT_HTML5, 'UTF-8');
88 }
89 }
90
91 return '&#'.$matches[1]; // Value will be unchanged because regex was /&#( )/
92}
93
103function testSqlAndScriptInject($val, $type)
104{
105 // Decode string first because a lot of things are obfuscated by encoding or multiple encoding.
106 // So <svg o&#110;load='console.log(&quot;123&quot;)' become <svg onload='console.log(&quot;123&quot;)'
107 // So "&colon;&apos;" become ":'" (due to ENT_HTML5)
108 // So "&Tab;&NewLine;" become ""
109 // So "&lpar;&rpar;" become "()"
110
111 // Loop to decode until no more things to decode.
112 //print "before decoding $val\n";
113 do {
114 $oldval = $val;
115 $val = html_entity_decode($val, ENT_QUOTES | ENT_HTML5); // Decode '&colon;', '&apos;', '&Tab;', '&NewLine', ...
116 // Sometimes we have entities without the ; at end so html_entity_decode does not work but entities is still interpreted by browser.
117 $val = preg_replace_callback(
118 '/&#(x?[0-9][0-9a-f]+;?)/i',
123 static function ($m) {
124 // Decode '&#110;', ...
126 },
127 $val
128 );
129
130 // We clean html comments because some hacks try to obfuscate evil strings by inserting HTML comments. Example: on<!-- -->error=alert(1)
131 $val = preg_replace('/<!--[^>]*-->/', '', $val);
132 $val = preg_replace('/[\r\n\t]/', '', $val);
133 } while ($oldval != $val);
134 //print "type = ".$type." after decoding: ".$val."\n";
135
136 $inj = 0;
137
138 // We check string because some hacks try to obfuscate evil strings by inserting non printable chars. Example: 'java(ascci09)scr(ascii00)ipt' is processed like 'javascript' (whatever is place of evil ascii char)
139 // We should use dol_string_nounprintableascii but function is not yet loaded/available
140 // Example of valid UTF8 chars:
141 // utf8 or utf8mb3: '\x09', '\x0A', '\x0D', '\x7E'
142 // utf8 or utf8mb3: '\xE0\xA0\x80'
143 // utf8mb4: '\xF0\x9D\x84\x9E' (so this may be refused by the database insert if pagecode is utf8=utf8mb3)
144 $newval = preg_replace('/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/u', '', $val); // /u operator makes UTF8 valid characters being ignored so are not included into the replace
145
146 // Note that $newval may also be completely empty '' when non valid UTF8 are found.
147 if ($newval != $val) {
148 // If $val has changed after removing non valid UTF8 chars, it means we have an evil string.
149 $inj += 1;
150 }
151 //print 'inj='.$inj.'-type='.$type.'-val='.$val.'-newval='.$newval."\n";
152
153 // For SQL Injection (only GET are used to scan for such injection strings)
154 if ($type == 1 || $type == 3) {
155 // Note the \s+ is replaced into \s* because some spaces may have been modified or removed in previous loop
156 $inj += preg_match('/delete[\/\*\s]*from/i', $val);
157 $inj += preg_match('/create[\/\*\s]*table/i', $val);
158 $inj += preg_match('/insert[\/\*\s]*into/i', $val);
159 $inj += preg_match('/select[\/\*\s]*from/i', $val);
160 $inj += preg_match('/from[\/\*\s]*dual/i', $val);
161 $inj += preg_match('/into[\/\*\s]*(outfile|dumpfile)/i', $val);
162 $inj += preg_match('/user[\/\*\s]*\‍(/i', $val); // avoid to use function user() or mysql_user() that return current database login
163 $inj += preg_match('/information_schema/i', $val); // avoid to use request that read information_schema database
164 $inj += preg_match('/<svg/i', $val); // <svg can be allowed in POST
165 $inj += preg_match('/update[^&=\w].*set.+=/i', $val); // the [^&=\w] test is to avoid error when request is like action=update&...set... or &updatemodule=...set...
166 $inj += preg_match('/union.+select/i', $val);
167 }
168 if ($type == 3) {
169 // Note the \s+ is replaced into \s* because some spaces may have been modified in previous loop
170 $inj += preg_match('/select|update|delete|truncate|replace|group\s*by|concat|count|from|union/i', $val);
171 }
172 if ($type != 2) { // Not common key strings, so we can check them both on GET and POST
173 $inj += preg_match('/updatexml\‍(/i', $val);
174 $inj += preg_match('/(\.\.%2f)+/i', $val);
175 $inj += preg_match('/\s@@/', $val);
176 }
177 // For XSS Injection done by closing textarea to execute content into a textarea field
178 $inj += preg_match('/<\/textarea/i', $val);
179 // For XSS Injection done by adding javascript with script
180 // This is all cases a browser consider text is javascript:
181 // When it found '<script', 'javascript:', '<style', 'onload\s=' on body tag, '="&' on a tag size with old browsers
182 // All examples on page: http://ha.ckers.org/xss.html#XSScalc
183 // More on https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
184 $inj += preg_match('/<embed/i', $val);
185 if (!defined('NOSCANAUDIOFORINJECTION')) {
186 $inj += preg_match('/<audio/i', $val);
187 }
188 if (!defined('NOSCANIFRAMEFORINJECTION')) {
189 $inj += preg_match('/<iframe/i', $val);
190 }
191 if (!defined('NOSCANOBJECTFORINJECTION')) {
192 $inj += preg_match('/<object/i', $val);
193 }
194 $inj += preg_match('/<script/i', $val);
195 $inj += preg_match('/Set\.constructor/i', $val); // ECMA script 6
196 if (!defined('NOSTYLECHECK')) {
197 $inj += preg_match('/<style/i', $val);
198 }
199 $inj += preg_match('/base\s+href/si', $val);
200 $inj += preg_match('/=data:/si', $val);
201
202 // List of dom events is on https://www.w3schools.com/jsref/dom_obj_event.asp and https://developer.mozilla.org/en-US/docs/Web/Events
203 $inj += preg_match('/on(abort|after|animation|auxclick|before|blur|bounce|cancel|canplay|canplaythrough|change|click|close|content|contextmenu|cuechange|copy|cut)[a-z]*\s*=/i', $val);
204 $inj += preg_match('/on(dblclick|drag|drop|durationchange|emptied|end|ended|error|focus|focusin|focusout|formdata|gotpointercapture|hashchange|input|invalid)[a-z]*\s*=/i', $val);
205 $inj += preg_match('/on(key|load|lostpointercapture|mouse)[a-z]*\s*=/i', $val); // onmousexxx can be set on img or any html tag like <img title='...' onmouseover=alert(1)>
206 $inj += preg_match('/on(offline|online|pagehide|pageshow|pointer)[a-z]*\s*=/i', $val);
207 $inj += preg_match('/on(paste|pause|play|playing|progress|ratechange|reset|resize|scroll|select|search|seeked|seeking|show|stalled|start|submit|suspend)[a-z]*\s*=/i', $val);
208 $inj += preg_match('/on(timeupdate|touch|transition|toggle|unload|volumechange|waiting|wheel)[a-z]*\s*=/i', $val);
209 // More not into the previous list
210 $inj += preg_match('/on(repeat|begin|finish)[a-z]*\s*=/i', $val);
211
212 // We refuse html into html because some hacks try to obfuscate evil strings by inserting HTML into HTML.
213 // Example: <img on<a>error=alert(1) or <img onerror<>=alert(1) to bypass test on onerror=
214 $tmpval = preg_replace('/<[^<]*>/', '', $val);
215
216 // List of dom events is on https://www.w3schools.com/jsref/dom_obj_event.asp and https://developer.mozilla.org/en-US/docs/Web/Events
217 $inj += preg_match('/on(mouse|drag|key|load|touch|pointer|select|transition)[a-z]*\s*=/i', $tmpval); // onmousexxx can be set on img or any html tag like <img title='...' onmouseover=alert(1)>
218 $inj += preg_match('/on(abort|after|animation|auxclick|before|blur|bounce|cancel|canplay|canplaythrough|change|click|close|contextmenu|cuechange|copy|cut)[a-z]*\s*=/i', $tmpval);
219 $inj += preg_match('/on(dblclick|drop|durationchange|emptied|end|ended|error|focus|focusin|focusout|formdata|gotpointercapture|hashchange|input|invalid)[a-z]*\s*=/i', $tmpval);
220 $inj += preg_match('/on(lostpointercapture|offline|online|pagehide|pageshow)[a-z]*\s*=/i', $tmpval);
221 $inj += preg_match('/on(paste|pause|play|playing|progress|ratechange|reset|resize|scroll|search|seeked|seeking|show|stalled|start|submit|suspend)[a-z]*\s*=/i', $tmpval);
222 $inj += preg_match('/on(timeupdate|toggle|unload|volumechange|waiting|wheel)[a-z]*\s*=/i', $tmpval);
223 // More not into the previous list
224 $inj += preg_match('/on(repeat|begin|finish)[a-z]*\s*=/i', $tmpval);
225
226 //$inj += preg_match('/on[A-Z][a-z]+\*=/', $val); // To lock event handlers onAbort(), ...
227 $inj += preg_match('/&#58;|&#0000058|&#x3A/i', $val); // refused string ':' encoded (no reason to have it encoded) to lock 'javascript:...'
228 $inj += preg_match('/j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*:/i', $val);
229 $inj += preg_match('/vbscript\s*:/i', $val);
230 // For XSS Injection done by adding javascript closing html tags like with onmousemove, etc... (closing a src or href tag with not cleaned param)
231 if ($type == 1 || $type == 3) {
232 $val = str_replace('enclosure="', 'enclosure=X', $val); // We accept enclosure=" for the export/import module
233 if (!defined("SECURITY_WAF_ALLOW_QUOTES_IN_GET") || !constant("SECURITY_WAF_ALLOW_QUOTES_IN_GET")) {
234 $inj += preg_match('/"/i', $val); // We refused " in GET parameters value.
235 }
236 }
237 if ($type == 2) {
238 $inj += preg_match('/[:;"\'<>\?\‍(\‍){}\$%#]/', $val); // PHP_SELF is a file system (or url path without parameters). It can contains spaces.
239 }
240
241 return $inj;
242}
243
252function analyseVarsForSqlAndScriptsInjection(&$var, $type, $stopcode = 1)
253{
254 if (is_array($var)) {
255 foreach ($var as $key => $value) { // Warning, $key may also be used for attacks
256 // Exclude check for some variable keys
257 if ($type === 0 && defined('NOSCANPOSTFORINJECTION') && is_array(constant('NOSCANPOSTFORINJECTION')) && in_array($key, (array) constant('NOSCANPOSTFORINJECTION'))) {
258 continue;
259 }
260
261 // Test on both the key (we force type to 1 for test on key, we must accept key like "delete=1" blocked with type 3) and the value
262 if (analyseVarsForSqlAndScriptsInjection($key, 1, $stopcode) && analyseVarsForSqlAndScriptsInjection($value, $type, $stopcode)) {
263 //$var[$key] = $value; // This is useless
264 } else {
265 http_response_code(403);
266
267 // Get remote IP: PS: We do not use getUserRemoteIP(), function is not yet loaded and we need a value that can't be spoofed
268 $ip = (empty($_SERVER['REMOTE_ADDR']) ? 'unknown' : $_SERVER['REMOTE_ADDR']);
269
270 if ($stopcode) {
271 $errormessage = 'Access refused to '.htmlentities($ip, ENT_COMPAT, 'UTF-8').' by SQL or Script injection protection in main.inc.php:analyseVarsForSqlAndScriptsInjection type='.htmlentities((string) $type, ENT_COMPAT, 'UTF-8');
272 //$errormessage .= ' paramkey='.htmlentities($key, ENT_COMPAT, 'UTF-8'); // Disabled to avoid text injection
273
274 $errormessage2 = 'page='.htmlentities((empty($_SERVER["REQUEST_URI"]) ? '' : $_SERVER["REQUEST_URI"]), ENT_COMPAT, 'UTF-8');
275 $errormessage2 .= ' paramtype='.htmlentities((string) $type, ENT_COMPAT, 'UTF-8');
276 $errormessage2 .= ' paramkey='.htmlentities($key, ENT_COMPAT, 'UTF-8');
277 $errormessage2 .= ' paramvalue='.htmlentities($value, ENT_COMPAT, 'UTF-8');
278
279 print $errormessage;
280 print "<br>\n";
281 print 'Try to go back, fix data of your form and resubmit it. You can contact also your technical support.';
282
283 print "\n".'<!--'."\n";
284 print $errormessage2;
285 print "\n".'-->';
286
287 // Add entry into the PHP server error log
288 if (function_exists('error_log')) {
289 error_log($errormessage.' '.substr($errormessage2, 2000));
290 }
291
292 // Note: No addition into security audit table is done because we don't want to execute code in such a case.
293 // Detection of too many such requests can be done with a fail2ban rule on 403 error code or into the PHP server error log.
294
295
296 if (class_exists('PHPUnit\Framework\TestSuite')) {
297 $message = $errormessage.' '.substr($errormessage2, 2000);
298 throw new Exception("Security injection exception: $message");
299 }
300 exit;
301 } else {
302 return false;
303 }
304 }
305 }
306 return true;
307 } else {
308 return (testSqlAndScriptInject($var, $type) <= 0);
309 }
310}
311
312// Prevent the use of method TRACE in case of the web server authorizes it (some do it by default). TRACE method can be used by attacker to steal cookies or other sensitive information.
313if (!empty($_SERVER["REQUEST_METHOD"]) && $_SERVER["REQUEST_METHOD"] == "TRACE") {
314 print 'Access refused with request method TRACE';
315 http_response_code(405);
316 exit();
317}
318
319// Sanity check on URL
320if (!defined('NOSCANPHPSELFFORINJECTION') && !empty($_SERVER["PHP_SELF"])) {
321 $morevaltochecklikepost = array($_SERVER["PHP_SELF"]);
322 analyseVarsForSqlAndScriptsInjection($morevaltochecklikepost, 2);
323}
324// Sanity check on GET parameters
325if (!defined('NOSCANGETFORINJECTION') && !empty($_SERVER["QUERY_STRING"])) {
326 // Note: QUERY_STRING is url encoded, but $_GET and $_POST are already decoded
327 // Because the analyseVarsForSqlAndScriptsInjection is designed for already url decoded value, we must decode QUERY_STRING
328 // Another solution is to provide $_GET as parameter with analyseVarsForSqlAndScriptsInjection($_GET, 1);
329 $morevaltochecklikeget = array(urldecode($_SERVER["QUERY_STRING"]));
330 analyseVarsForSqlAndScriptsInjection($morevaltochecklikeget, 1);
331}
332// Sanity check on POST
333if (!defined('NOSCANPOSTFORINJECTION') || is_array(constant('NOSCANPOSTFORINJECTION'))) {
335}
testSqlAndScriptInject($val, $type)
Security: WAF layer for SQL Injection and XSS Injection (scripts) protection (Filters on GET,...
Definition waf.inc.php:103
getArrayOfEmoji()
Return array of Emojis.
Definition waf.inc.php:42
realCharForNumericEntities($matches)
Return the real char for a numeric entities.
Definition waf.inc.php:66
analyseVarsForSqlAndScriptsInjection(&$var, $type, $stopcode=1)
Return true if security check on parameters are OK, false otherwise.
Definition waf.inc.php:252