Project

General

Profile

1
<?php
2
/**
3
 *
4
 * @category        framework
5
 * @package         SecureForm.mtab
6
 * @author          WebsiteBaker Community Project
7
 * @copyright       2004-2009, Ryan Djurovich
8
 * @copyright       2009-2011, Website Baker Org. e.V.
9
 * @link			http://www.websitebaker2.org/
10
 * @license         http://www.gnu.org/licenses/gpl.html
11
 * @platform        WebsiteBaker 2.8.2
12
 * @requirements    PHP 5.2.2 and higher
13
 * @version         $Id: SecureForm.mtab.php 2106 2014-11-24 18:24:58Z darkviper $
14
 * @filesource		$HeadURL: svn://isteam.dynxs.de/wb-archiv/branches/2.8.x/wb/framework/SecureForm.mtab.php $
15
 * @lastmodified    $Date: 2014-11-24 19:24:58 +0100 (Mon, 24 Nov 2014) $
16
 * @description
17
 */
18
##  Heavy patched version, idea for patches based on :
19
##  http://stackoverflow.com/questions/2695153/php-csrf-how-to-make-it-works-in-all-tabs/2695291#2695291
20
##  Whith this patch the token System now allows for multiple browser tabs but
21
##  denies the use of multiple browsers.
22
##  You can configure this class by adding several constants to your config.php
23
##  All Patches are Copyright Norbert Heimsath released under GPLv3
24
##  http://www.gnu.org/licenses/gpl.html
25
##  Take a look at  __construkt  for configuration options(constants).
26
##  Patch version 0.3.5
27

    
28
/**
29
 *
30
 * If you want some special configuration put this somewhere in your config.php for
31
 * example or just uncomment the lines here
32
 *
33
 * This parameter now can be set with the admintool SecureForm Switcher coded by Luisehahne,
34
 * pls ask for it in the forum
35
 *
36
 * Secret can contain anything its the base for the secret part for the hash
37
 * define ('WB_SECFORM_SECRET','whatever you like');
38
 * after how many seconds a new secret is generated
39
 * define ('WB_SECFORM_SECRETTIME',86400);      #aprox one day
40
 * shall we use fingerprinting true/false
41
 * define ('WB_SECFORM_USEFP', true);
42
 * Timeout till the form token times out. Integer value between 0-86400 seconds (one day)
43
 * define ('WB_SECFORM_TIMEOUT', 3600);
44
 * Name for the token form element only alphanumerical string allowed that starts whith a charakter
45
 * define ('WB_SECFORM_TOKENNAME','my3form3');
46
 * how many blocks of the IP should be used in fingerprint 0=no ipcheck, possible values 0-4
47
 * define ('FINGERPRINT_WITH_IP_OCTETS',2);
48
 *
49
 * Fixed IDKEY in Secureform 2012/08/27 by NorHey
50
 *
51
 * I still had problems whith Security warnings on pages that used alot of
52
 * IDKEYS. For example if i tried to move a field in the basic form module
53
 * i had one  Warning on in about 15 atempts.
54
 *
55
 * Another Problem: unused old IDKEYS where never deleted so session got
56
 * spammed by idkeys.
57
 *
58
 * So i made a more unique ID, recursive checking for overlapping ids,
59
 * added a tiemout for IDKEYS (same as for FTAN) and added a page parameter
60
 * to instantly delete all keys from a pagecall if one is used.
61
 *
62
 * Last thing i added is an additional parameter that allows the use of
63
 * IDKEYS whith ajax scripts. If this setting is used, the key is not
64
 * removed and can be used multiple times. (this is a small security risk
65
 * but nothing compared to the 1 in 16 chance to guess the key that we had before.)
66
 *
67
 */
68

    
69
/* -------------------------------------------------------- */
70
// Must include code to stop this file being accessed directly
71
if(!defined('WB_PATH')) {
72
	require_once(dirname(__FILE__).'/globalExceptionHandler.php');
73
	throw new IllegalFileException();
74
}
75
/* -------------------------------------------------------- */
76

    
77
class SecureForm {
78

    
79
	const FRONTEND = 0;
80
	const BACKEND  = 1;
81

    
82
        ## additional private data
83
	private $_secret      	 = '5609bnefg93jmgi99igjefg';
84
	private $_secrettime  	 = 86400;   #Approx. one day
85
        private $_tokenname   	 = 'formtoken';
86
	private $_timeout	 = 7200;
87
	private $_useipblocks	 = 2;
88
	private $_usefingerprint = true;
89
	private $_page_uid = "";
90
	private $_page_id = "";
91
        ### additional private data
92

    
93
        private $_FTAN           = '';
94
	private $_IDKEYs         = array('0'=>'0');
95
	private $_idkey_name     = '';
96
	private $_salt           = '';
97
	private $_fingerprint    = '';
98
	private $_serverdata  	 = '';
99

    
100
	/* Construtor */
101
	protected function __construct($mode = self::FRONTEND){
102

    
103
        	// additional constants and stuff for global configuration
104

    
105
		// Secret can contain anything its the base for the secret part of the hash
106
                if (defined ('WB_SECFORM_SECRET')){
107
			$this->_secret=WB_SECFORM_SECRET;
108
		}
109

    
110
		// shall we use fingerprinting
111
                if (defined ('WB_SECFORM_USEFP') AND WB_SECFORM_USEFP===false){
112
			$this->_usefingerprint	= false;
113
		}
114

    
115
                // Timeout till the form token times out. Integer value between 0-86400 seconds (one day)
116
                if (defined ('WB_SECFORM_TIMEOUT') AND is_numeric(WB_SECFORM_TIMEOUT) AND intval(WB_SECFORM_TIMEOUT) >=0 AND intval(WB_SECFORM_TIMEOUT) <=86400 ){
117
			$this->_timeout=intval(WB_SECFORM_TIMEOUT);
118
		}
119
		// Name for the token form element only alphanumerical string allowed that starts whith a charakter
120
                if (defined ('WB_SECFORM_TOKENNAME') AND !$this->_validate_alalnum(WB_SECFORM_TOKENNAME)){
121
			$this->_tokenname=WB_SECFORM_TOKENNAME;
122
		}
123
		// how many bloks of the IP should be used 0=no ipcheck
124
                if (defined ('FINGERPRINT_WITH_IP_OCTETS') AND !$this->_is04(FINGERPRINT_WITH_IP_OCTETS)){
125
			$this->_useipblocks=FINGERPRINT_WITH_IP_OCTETS;
126
                }
127

    
128
		// create a unique pageid for per page management of idkeys , especially
129
		// to identify and delete unused ones from same page call.
130
		$this->_page_uid = uniqid (rand(), true);
131

    
132
		// additional stuff end
133
		$this->_browser_fingerprint   = $this->_browser_fingerprint(true);
134
		$this->_fingerprint   = $this->_generate_fingerprint();
135
		$this->_serverdata    = $this->_generate_serverdata();
136
		$this->_secret        = $this->_generate_secret();
137
                $this->_salt          = $this->_generate_salt();
138

    
139
		$this->_idkey_name    = substr($this->_fingerprint, hexdec($this->_fingerprint[strlen($this->_fingerprint)-1]), 16);
140
		// make sure there is a alpha-letter at first position
141
		$this->_idkey_name[0] = dechex(10 + (hexdec($this->_idkey_name[0]) % 5));
142
		// takeover id_keys from session if available
143
		if(isset($_SESSION[$this->_idkey_name]) && is_array($_SESSION[$this->_idkey_name])){
144
			$this->_IDKEYs = $_SESSION[$this->_idkey_name];
145
		}else{
146
			$this->_IDKEYs = array('0'=>'0');
147
			$_SESSION[$this->_idkey_name] = $this->_IDKEYs;
148
		}
149
	}
150

    
151
	private function _generate_secret(){
152

    
153
                $secret= $this->_secret;
154
		$secrettime= $this->_secrettime;
155
		#create a different secret every day
156
		$TimeSeed= floor(time()/$secrettime)*$secrettime;  #round(floor) time() to whole days
157
		$DomainSeed =  $_SERVER['SERVER_NAME'];  # generate a numerical from server name.
158
		$Seed = $TimeSeed+$DomainSeed;
159
                $secret .=md5($Seed);  #
160

    
161
		$secret .= $this->_secret.$this->_serverdata.session_id();
162
		if ($this->_usefingerprint){$secret.= $this->_browser_fingerprint;}
163

    
164
	return $secret;
165
	}
166

    
167

    
168

    
169
	private function _generate_salt()
170
		{
171
			if(function_exists('microtime'))
172
			{
173
				list($usec, $sec) = explode(" ", microtime());
174
				$salt = (string)((float)$usec + (float)$sec);
175
			}else{
176
				$salt = (string)time();
177
			}
178
			$salt = (string)rand(10000, 99999) . $salt . (string)rand(10000, 99999);
179
			return md5($salt);
180
		}
181

    
182
	private function _generate_fingerprint()
183
	{
184
	// server depending values
185
 		$fingerprint  = $this->_generate_serverdata();
186

    
187
	// client depending values
188
		$fingerprint .= ( isset($_SERVER['HTTP_USER_AGENT']) ) ? $_SERVER['HTTP_USER_AGENT'] : '17';
189
		$usedOctets = ( defined('FINGERPRINT_WITH_IP_OCTETS') ) ? intval(defined('FINGERPRINT_WITH_IP_OCTETS')) : 0;
190
		$clientIp = ( isset($_SERVER['REMOTE_ADDR'])  ? $_SERVER['REMOTE_ADDR'] : '' );
191
		if(($clientIp != '') && ($usedOctets > 0)){
192
			$ip = explode('.', $clientIp);
193
			while(sizeof($ip) > $usedOctets) { array_pop($ip); }
194
			$clientIp = implode('.', $ip);
195
		}else {
196
			$clientIp = 19;
197
		}
198
		$fingerprint .= $clientIp;
199
		return md5($fingerprint);
200
	}
201

    
202
	private function _generate_serverdata(){
203

    
204
		$usedOctets = ( defined('FINGERPRINT_WITH_IP_OCTETS') ) ? (intval(FINGERPRINT_WITH_IP_OCTETS) % 5) : 2;
205
		$serverdata  = '';
206
	 	$serverdata .= ( isset($_SERVER['SERVER_SIGNATURE']) ) ? $_SERVER['SERVER_SIGNATURE'] : '2';
207
		$serverdata .= ( isset($_SERVER['SERVER_SOFTWARE']) ) ? $_SERVER['SERVER_SOFTWARE'] : '3';
208
		$serverdata .= ( isset($_SERVER['SERVER_NAME']) ) ? $_SERVER['SERVER_NAME'] : '5';
209
		$serverIp = ( isset($_SERVER['SERVER_ADDR']) ) ? $_SERVER['SERVER_ADDR'] : '';
210
		if(($serverIp != '') && ($usedOctets > 0)){
211
			$ip = explode('.', $serverIp);
212
			while(sizeof($ip) > $usedOctets) { array_pop($ip); }
213
			$serverdata .= implode('.', $ip);
214
		}else {
215
			$serverdata .= '7';
216
		}
217
		$serverdata .= ( isset($_SERVER['SERVER_PORT']) ) ? $_SERVER['SERVER_PORT'] : '11';
218
		$serverdata .= ( isset($_SERVER['SERVER_ADMIN']) ) ? $_SERVER['SERVER_ADMIN'] : '13';
219
		$serverdata .= PHP_VERSION;
220
	return  $serverdata;
221
	}
222

    
223
        // fake funktion , just exits to avoid error message
224
        final protected function createFTAN(){}
225

    
226
	/*
227
	* creates selfsigning Formular transactionnumbers for unique use
228
	* @access public
229
	* @param bool $asTAG: true returns a complete prepared, hidden HTML-Input-Tag (default)
230
	*                     false returns an GET argument 'key=value'
231
	* @return mixed:      string
232
	*
233
	* requirements: an active session must not be available but it makes no sense whithout :-)
234
	*/
235
	final public function getFTAN( $as_tag = true)
236
	{
237
		$secret= $this->_secret;
238

    
239
		$timeout= time()+$this->_timeout;
240

    
241
		#mt_srand(hexdec(crc32(microtime()));
242
                $token= dechex(mt_rand());
243

    
244
                $hash= sha1($secret.'-'.$token.'-'.$timeout);
245
		$signed= $token.'-'.$timeout.'-'.$hash;
246

    
247
		if($as_tag == true)
248
		{ // by default return a complete, hidden <input>-tag
249
			return '<input type="hidden" name="'.$this->_tokenname.'" value="'.htmlspecialchars($signed).'" title=" " />';
250
		}else{ // return an array with raw tokenname=value
251
			return $this->_tokenname.'='.$signed;
252
		}
253
	}
254

    
255
	/*
256
	* checks received form-transactionnumbers against itself
257
	* @access public
258
	* @param string $mode: requestmethode POST(default) or GET
259
	* @return bool:    true if numbers matches against stored ones
260
	*
261
	* requirements: no active session must be available but it makes no sense whithout.
262
	* this check will prevent from multiple sending a form. history.back() also will never work
263
	*/
264
	final public function checkFTAN( $mode = 'POST')
265
	{
266
		$mode = (strtoupper($mode) != 'POST' ? '_GET' : '_POST');
267

    
268
		$isok= false;
269
		$secret= $this->_secret;
270

    
271
		if (isset($GLOBALS[$mode][$this->_tokenname])) 	{$latoken=$GLOBALS[$mode][$this->_tokenname];}
272
                else 						{return $isok;}
273

    
274
		$parts= explode('-', $latoken);
275
		if (count($parts)==3) {
276
			list($token,$timeout, $hash)= $parts;
277
			if ($hash==sha1($secret.'-'.$token.'-'.$timeout) AND $timeout > time())
278
			{$isok= true;}
279
		}
280

    
281
		return $isok;
282
	}
283

    
284
	/*
285
	* save values in session and returns a ID-key
286
	* @access public
287
	* @param mixed $value: the value for witch a key shall be generated and memorized
288
	* @return string:      a MD5-Key to use instead of the real value
289
	*
290
	* @requirements: an active session must be available
291
	* @description: IDKEY can handle string/numeric/array - vars. Each key is a
292
	*  shortened md5 String
293
	*/
294
	final public function getIDKEY($value) {
295

    
296

    
297
		// serialize value, if it's an array
298
		if( is_array($value) == true ) $value = serialize($value);
299

    
300
		// cryptsome random stuff with salt into md5-hash
301
		$key = md5($this->_salt.rand().uniqid('', true));
302

    
303
		//shorten hash a bit
304
		//$key = str_replace(array("=","$","+","/"),array("","","",""),base64_encode(pack('H*',$key)));
305
        $regex = "/[$+= \/-]/"; // 20120831 dw
306
        $replace = "";
307
        $key = preg_replace ($regex, $replace, base64_encode(pack('H*',$key)), -1 );
308

    
309
		// the key is unique, so store it in list
310
		if( !array_key_exists($key,  $_SESSION[$this->_idkey_name])) {
311
			$_SESSION[$this->_idkey_name][$key]['value'] = $value;
312
			$_SESSION[$this->_idkey_name][$key]['time'] = time();
313
			$_SESSION[$this->_idkey_name][$key]['page'] = $this->_page_uid;
314
		}
315
		// if key already exist, try again, dont store as it already been stored
316
		else $key = $this->getIDKEY($value);
317

    
318
		return $key;
319
	}
320

    
321
	/*
322
	* search for key in session and returns the original value
323
	* @access public
324
	* @param string $fieldname: name of the POST/GET-Field containing the key or hex-key itself
325
	* @param mixed $default: returnvalue if key not exist (default 0)
326
	* @param string $request: requestmethode can be POST or GET or '' (default POST)
327
	* @return mixed: the original value (string, numeric, array) or DEFAULT if request fails
328
	*
329
	* @requirements: an active session must be available
330
	* @description: each IDKEY can be checked only once. Unused Keys stay in list until the
331
	*               session is destroyed.
332
	*/
333
 	final public function checkIDKEY( $fieldname, $default = 0, $request = 'POST', $ajax=false)
334
	{
335
		//Remove timed out idkeys
336
		$_SESSION[$this->_idkey_name]= array_filter( $_SESSION[$this->_idkey_name],array($this, "_timedout"));
337

    
338

    
339
		$return_value = $default; // set returnvalue to default
340
		switch( strtoupper($request) )
341
		{
342
			case 'POST':
343
				$key = isset($_POST[$fieldname]) ? $_POST[$fieldname] : $fieldname;
344
				break;
345
			case 'GET':
346
				$key = isset($_GET[$fieldname]) ? $_GET[$fieldname] : $fieldname;
347
				break;
348
			default:
349
				$key = $fieldname;
350
		}
351
		if( preg_match('/[0-9a-zA-Z\-\_]{1,32}$/', $key) ) { // key must match the generated ones
352
			if( array_key_exists($key, $_SESSION[$this->_idkey_name])) { // check if key is stored in IDKEYs-list
353
				$return_value = $_SESSION[$this->_idkey_name][$key]['value']; // get stored value
354
				$this->_page_id = $_SESSION[$this->_idkey_name][$key]['page'];
355

    
356
				if ($ajax === false) {
357
					//Remove all keys from this page to prevent multiuse and session mem usage
358
					$_SESSION[$this->_idkey_name]= array_filter( $_SESSION[$this->_idkey_name],array($this, "_fpages"));
359
					//unset($_SESSION[$this->_idkey_name][$key]);   // remove from list to prevent multiuse
360
				}
361
				if( preg_match('/.*(?<!\{).*(\d:\{.*;\}).*(?!\}).*/', $return_value) )
362
				{ // if value is a serialized array, then deserialize it
363
					$return_value = unserialize($return_value);
364
				}
365
			}
366
		}
367

    
368
		return $return_value;
369
	}
370

    
371
 	private function _timedout( $var ) {
372
        if(!isset($var['time'])) { return false; }
373
		if ($var['time'] < time()-$this->_timeout) return false;
374
		return true;
375
	}
376
 	private function _fpages( $var ) {
377
		if ($var['page'] == $this->_page_id) return false;
378
		return true;
379
	}
380

    
381
	/* @access public
382
	* @return void
383
	*
384
	* @requirements: an active session must be available
385
	* @description: remove all entries from IDKEY-Array
386
	*
387
	*/
388
 	final public function clearIDKEY()
389
	{
390
		 $this->_IDKEYs = array('0'=>'0');
391
	}
392

    
393

    
394
	## additional Functions needed cause the original ones lack some functionality
395
	## all are Copyright Norbert Heimsath, heimsath.org
396
	## released under GPLv3  http://www.gnu.org/licenses/gpl.html
397

    
398
	/* Made because ctype_ gives strange results using mb Strings*/
399
 	private function _validate_alalnum($input){
400
	# alphanumerical string that starts whith a letter charakter
401
		if (preg_match('/^[a-zA-Z][0-9a-zA-Z]+$/u', $input))
402
			{return false;}
403

    
404
	return "The given input is not an alphanumeric string.";
405
	}
406

    
407
 	private function _is04($input){
408
	# integer value between 0-4
409
		if (preg_match('/^[0-4]$/', $input)) {return false;}
410

    
411
	return "The given input is not an alphanumeric string.";
412
	}
413

    
414

    
415
	private function _getip($ipblocks=4){
416
	/*
417
	Just a function to get User ip even if hes behind a proxy
418
	*/
419
		$ip    	=   ""; //Ip address result
420
		$cutip	=   ""; //Ip address cut to limit
421

    
422
		# mabe user is behind a Proxy but we need his real ip address if we got a nice Proxyserver,
423
		# it sends us the "HTTP_X_FORWARDED_FOR" Header. Sometimes there is more than one Proxy.
424
		# !!!!!! THIS PART WAS NEVER TESTED BECAUSE I ONLY GOT A DIRECT INTERNET CONNECTION !!!!!!
425
		# long2ip(ip2long($lastip)) makes sure we got nothing else than an ip into our script ;-)
426
		# !!!!! WARNING the 'HTTP_X_FORWARDED_FOR' Part is NOT TESTED !!!!!
427
		if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) AND !empty($_SERVER['HTTP_X_FORWARDED_FOR']))
428
		{
429
			$iplist= explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
430
			$lastip = array_pop($iplist);
431
			$ip.= long2ip(ip2long($lastip));
432
		}
433

    
434
		/* If theres no other supported info we just use REMOTE_ADDR
435
		If we have a fiendly proxy supporting  HTTP_X_FORWARDED_FOR its ok to use the full address.
436
		But if there is no HTTP_X_FORWARDED_FOR we can  not be sure if its a proxy or whatever, so we use the
437
		blocklimit for IP address.
438
		*/
439
		else
440
		{
441
			$ip = long2ip(ip2long($_SERVER['REMOTE_ADDR']));
442

    
443
			# ipblocks used here defines how many blocks of the ip adress are checked xxx.xxx.xxx.xxx
444
			$blocks = explode('.', $ip);
445
			for ($i=0; $i<$ipblocks; $i++){
446
				$cutip.= $blocks[$i] . '.';
447
				}
448
			$ip=substr($cutip, 0, -1);
449
		}
450

    
451
	return $ip;
452
	}
453

    
454
	private function _browser_fingerprint($encode=true,$fpsalt="My Fingerprint: "){
455
	/*
456
	Creates a basic Browser Fingerprint for securing the session and forms.
457
	*/
458

    
459
		$fingerprint=$fpsalt;
460
		if (isset($_SERVER['HTTP_USER_AGENT'])){ $fingerprint .= $_SERVER['HTTP_USER_AGENT'];}
461
		if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])){ $fingerprint .= $_SERVER['HTTP_ACCEPT_LANGUAGE'];}
462
//		if (isset($_SERVER['HTTP_ACCEPT_ENCODING'])){ $fingerprint .= $_SERVER['HTTP_ACCEPT_ENCODING'];}
463
		if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])){ $fingerprint .= $_SERVER['HTTP_ACCEPT_CHARSET'];}
464

    
465
		$fingerprint.= $this->_getip($this->_useipblocks);
466

    
467
		if ($encode){$fingerprint=md5($fingerprint);}
468

    
469
	return $fingerprint;
470
	}
471
	##
472
	## additional Functions END
473
	##
474
}
(8-8/38)