mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-01-06 22:19:25 +01:00
190 lines
7.4 KiB
PHP
Executable File
190 lines
7.4 KiB
PHP
Executable File
<?php
|
|
/**
|
|
* EGroupware XSS protection and other security relevant functions
|
|
*
|
|
* Usually loaded via header.inc.php or api/src/loader/common.php
|
|
*
|
|
* @link http://www.egroupware.org
|
|
* @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
|
|
* @package api
|
|
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
|
* @version $Id$
|
|
*/
|
|
|
|
use EGroupware\Api;
|
|
|
|
/**
|
|
* check $_REQUEST data for XSS, vars containing script tags are moved to $GLOBALS['egw_unset_vars']
|
|
*
|
|
* @internal
|
|
* @param array &$var reference of array to check
|
|
* @param string $name ='' name of the array
|
|
* @param boolean $log = true Log the results of checking to the error log
|
|
*/
|
|
function _check_script_tag(&$var,$name='',$log=true)
|
|
{
|
|
static $preg=null;
|
|
//old: '/<\/?[^>]*\b(iframe|script|javascript|on(before)?(abort|blur|change|click|dblclick|error|focus|keydown|keypress|keyup|load|mousedown|mousemove|mouseout|mouseover|mouseup|reset|select|submit|unload))\b[^>]*>/i';
|
|
if (!isset($preg)) $preg =
|
|
// forbidden tags like iframe or script
|
|
'/(<(\s*\/)?\s*(iframe|script|object|embed|math|meta)[^a-z0-9]|'.
|
|
// on* attributes
|
|
'<[^>]*on(before)?(abort|blur|change|click|dblclick|error|focus|keydown|keypress|keyup|load|mouse(out|enter|leave|over|move|up|wheel|down)'.
|
|
'|cached|beforeunload|online|offline|open|message|close|animation(start|end|iteration)|transition(start|end|run)|reset'.
|
|
'|beforeprint|afterprint|composition(start|update|end)|fullscreenchange|fullscreenerror|cut|copy|auxclick|contextmenu'.
|
|
'|wheel|drag(start|end|enter|over|leave)|drop|loadstart|progress|timeout|loadendreset|select|submit|unload|resize'.
|
|
'|propertychange|page(hide|show)|scroll|readystatechange|start|popstate|form|input)\s*=|'.
|
|
// ="javascript:*" diverse javascript attribute value
|
|
'<[^>]+(href|src|dynsrc|lowsrc|background|style|poster|action)\s*=\s*("|\')?[^"\']*javascript|'.
|
|
// benavior:url and expression in style attribute
|
|
'<[^>]+style\s*=\s*("|\')[^>]*(behavior\s*:\s*url|expression)\s*\()/i';
|
|
if (is_array($var))
|
|
{
|
|
foreach($var as $key => $val)
|
|
{
|
|
if (is_array($val))
|
|
{
|
|
_check_script_tag($var[$key],$name.'['.$key.']');
|
|
}
|
|
elseif(strpos($val, '<') !== false) // speedup: ignore everything without <
|
|
{
|
|
if (preg_match($preg,$val))
|
|
{
|
|
// special handling for $_POST[json_data], to decend into it's decoded content, fixing json direct might break json syntax
|
|
if ($name == '_POST' && $key == 'json_data' && ($json_data = json_decode($val, true)))
|
|
{
|
|
_check_script_tag($json_data, $name.'[json_data]');
|
|
$_REQUEST[$key] = $var[$key] = json_encode($json_data);
|
|
continue;
|
|
}
|
|
//error_log(__FUNCTION__."(,$name) ${name}[$key] = ".$var[$key]);
|
|
$GLOBALS['egw_unset_vars'][$name.'['.$key.']'] = $var[$key];
|
|
// attempt to clean the thing
|
|
$var[$key] = $val = Api\Html\HtmLawed::purify($val);
|
|
// check if we succeeded, if not drop the var anyway, keep the egw_unset_var in any case
|
|
if (preg_match($preg,$val))
|
|
{
|
|
if($log)
|
|
{
|
|
error_log("*** _check_script_tag($name): unset(${name}[$key]) with value $val***");
|
|
}
|
|
unset($var[$key]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// in case some stupid old code expects the array-pointer to be at the start of the array
|
|
reset($var);
|
|
}
|
|
}
|
|
|
|
foreach(array('_GET','_POST','_REQUEST','HTTP_GET_VARS','HTTP_POST_VARS') as $n => $where)
|
|
{
|
|
$pregs = array(
|
|
'order' => '/^[a-zA-Z0-9_,]*$/',
|
|
'sort' => '/^(ASC|DESC|asc|desc|0|1|2|3|4|5|6|7){0,1}$/',
|
|
);
|
|
foreach(array('order','sort') as $name)
|
|
{
|
|
if (isset($GLOBALS[$where][$name]) && !is_array($GLOBALS[$where][$name]) && !preg_match($pregs[$name],$GLOBALS[$where][$name]))
|
|
{
|
|
$GLOBALS[$where][$name] = '';
|
|
}
|
|
}
|
|
// do the check for script-tags only for _GET and _POST or if we found something in _GET and _POST
|
|
// speeds up the execusion a bit
|
|
if (isset($GLOBALS[$where]) && is_array($GLOBALS[$where]) && ($n < 2 || isset($GLOBALS['egw_unset_vars'])))
|
|
{
|
|
_check_script_tag($GLOBALS[$where],$where);
|
|
}
|
|
}
|
|
//if (is_array($GLOBALS['egw_unset_vars'])) { echo "egw_unset_vars=<pre>".htmlspecialchars(print_r($GLOBALS['egw_unset_vars'],true))."</pre>"; exit; }
|
|
|
|
// $GLOBALS[egw_info][flags][currentapp] and die if it contains something nasty or unexpected
|
|
if (isset($GLOBALS['egw_info']) && isset($GLOBALS['egw_info']['flags']) &&
|
|
isset($GLOBALS['egw_info']['flags']['currentapp']) && !preg_match('/^[A-Za-z0-9_-]+$/',$GLOBALS['egw_info']['flags']['currentapp']))
|
|
{
|
|
error_log(__FILE__.': '.__LINE__.' Invalid $GLOBALS[egw_info][flags][currentapp]='.array2string($GLOBALS['egw_info']['flags']['currentapp']).', $_SERVER[REQUEST_URI]='.array2string($_SERVER[REQUEST_URI]));
|
|
die('Invalid $GLOBALS[egw_info][flags][currentapp]!');
|
|
}
|
|
|
|
// neutralises register_globals On, which is not used by eGW
|
|
// some code from the hardend php project: http://www.hardened-php.net/articles/PHPUG-PHP-Sicherheit-Parametermanipulationen.pdf
|
|
if (ini_get('register_globals'))
|
|
{
|
|
function unregister_globals()
|
|
{
|
|
// protect against GLOBALS overwrite or setting egw_info
|
|
if (isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS']) || isset($_REQUEST['egw_info']) || isset($_FILES['egw_info']))
|
|
{
|
|
die('GLOBALS overwrite detected!!!');
|
|
}
|
|
// unregister all globals
|
|
$noUnset = array('GLOBALS','_GET','_POST','_COOKIE','_SERVER','_ENV','_FILES','xajax');
|
|
foreach(array_unique(array_merge(
|
|
array_keys($_GET),array_keys($_POST),array_keys($_COOKIE),array_keys($_SERVER),array_keys($_ENV),array_keys($_FILES),
|
|
isset($_SESSION) && is_array($_SESSION) ? array_keys($_SESSION) : array())) as $k)
|
|
{
|
|
if (!in_array($k,$noUnset) && isset($GLOBALS[$k]))
|
|
{
|
|
unset($GLOBALS[$k]);
|
|
}
|
|
}
|
|
}
|
|
unregister_globals();
|
|
}
|
|
|
|
/**
|
|
* Unserialize a php serialized string, but only if it contains NO objects 'O:\d:"' or 'C:\d:"' pattern
|
|
*
|
|
* Should be used for all external content, to guard against exploidts.
|
|
*
|
|
* PHP 7.0+ can be told not to instanciate any classes (and calling eg. it's destructor).
|
|
* In fact it instanciates it as __PHP_Incomplete_Class without any methods and therefore disarming threads.
|
|
*
|
|
* @param string $str
|
|
* @return mixed
|
|
*/
|
|
function php_safe_unserialize($str)
|
|
{
|
|
if (PHP_VERSION >= 7)
|
|
{
|
|
return unserialize($str, array('allowed_classes' => false));
|
|
}
|
|
if ((strpos($str, 'O:') !== false || strpos($str, 'C:') !== false) &&
|
|
preg_match('/(^|;|{)[OC]:\d+:"/', $str))
|
|
{
|
|
error_log(__METHOD__."('$str') contains objects --> return NULL");
|
|
return null; // null, not false, to not trigger behavior of returning string itself to app code
|
|
}
|
|
return unserialize($str);
|
|
}
|
|
|
|
/**
|
|
* Unserialize a json or php serialized array
|
|
*
|
|
* Used to migrate from PHP serialized database values to json-encoded ones.
|
|
*
|
|
* @param string $str string with serialized array
|
|
* @param boolean $allow_not_serialized =false true: return $str as is, if it is no serialized array
|
|
* @return array|str|false false if content can not be unserialized (not null like json_decode!)
|
|
*/
|
|
function json_php_unserialize($str, $allow_not_serialized=false)
|
|
{
|
|
if ((in_array($str[0], array('a', 'i', 's', 'b', 'O', 'C')) && $str[1] == ':' || $str === 'N;') &&
|
|
($arr = php_safe_unserialize($str)) !== false || $str === 'b:0;')
|
|
{
|
|
return $arr;
|
|
}
|
|
if (!$allow_not_serialized || $str[0] == '[' || $str[0] == '{' || $str[0] == '"' || $str === 'null' || ($val = json_decode($str, true)) !== null)
|
|
{
|
|
// json_decode return null, if it cant decode the content
|
|
if (isset($val) || ($val = json_decode($str, true)) !== null || $str === 'null')
|
|
{
|
|
return $val;
|
|
}
|
|
return false;
|
|
}
|
|
return $str;
|
|
}
|