Skip to content

Commit 936fc2b

Browse files
committed
Add HTML Purifier and XSS Security
1 parent 9dab43e commit 936fc2b

File tree

6 files changed

+102
-12
lines changed

6 files changed

+102
-12
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"ext-mbstring": "*",
2121
"ext-openssl": "*",
2222
"ext-gd": "*",
23-
"ext-pdo": "*"
23+
"ext-pdo": "*",
24+
"ezyang/htmlpurifier": "^4.17"
2425
},
2526
"require-dev": {
2627
"phpunit/phpunit": "^10.5 || ^11.0"

config/security.config.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,16 @@
228228
*/
229229
define("USE_HTMLPURIFIER", false);
230230

231+
/*Old implementation without composer (by using htmlpurifier folder)
231232
if (XSS_PROTECTION) {
232233
if (USE_HTMLPURIFIER) {
233234
require_once(RELATIVE_PATH . 'framework/htmlpurifier/HTMLPurifier.auto.php');
234235
}
235236
}
236-
237+
*/
237238
/**
238239
* Securing forms
239-
* Specifies csrftoken token fields for Record Component
240+
* Specifies the csrftoken token fields for Record Component
240241
*/
241242

242243
define("CSRF_TOKEN_FORM_FIELD", "csrftoken");

framework/Controller.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -505,15 +505,15 @@ protected function restrictToAuthentication($redirect = null, $returnLink = null
505505

506506
public function xssClean(&$data,$charset = CHARSET)
507507
{
508-
/* TODO
508+
509509
if (is_array($data)){
510510
foreach ($data as $k => $v) {
511511
$data[$k] = $this->view->xssCleanString($v, $charset);
512512
}
513513
} else {
514514
$this->view->xssCleanString($data, $charset);
515515
}
516-
*/
516+
517517
}
518518

519519
/**

framework/View.php

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,9 @@
3636
use framework\exceptions\NotInitializedViewException;
3737
use framework\exceptions\BlockNotFoundException;
3838

39-
/** TODO
40-
* use HTMLPurifier;
41-
* use HTMLPurifier_Config;
42-
*/
39+
use HTMLPurifier;
40+
use HTMLPurifier_Config;
41+
4342

4443
class View
4544
{
@@ -425,7 +424,7 @@ public function getTplFileName()
425424
return $this->tplFileName;
426425
}
427426

428-
/** TODO
427+
/**
429428
* Cleans a string against XSS attack
430429
*
431430
* @param string $string String to purify
@@ -434,6 +433,42 @@ public function getTplFileName()
434433
*/
435434
public function xssCleanString($string, $charset = CHARSET)
436435
{
437-
return null;
436+
437+
if (!USE_HTMLPURIFIER) {
438+
// Fix &entity\n;
439+
$string = str_replace(array('&', '<', '>'), array('&', '<', '>'), $string);
440+
$string = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '$1;', $string);
441+
$string = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $string);
442+
$string = html_entity_decode($string, ENT_COMPAT, $charset);
443+
444+
// Remove any attribute starting with "on" or xmlns
445+
$string = preg_replace('#(<[^>]+?[\x00-\x20"\'])(?:on|xmlns)[^>]*+>#iu', '$1>', $string);
446+
447+
// Remove javascript: and vbscript: protocols
448+
$string = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $string);
449+
$string = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $string);
450+
$string = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding...', $string);
451+
452+
// Only works in IE: <span style="width: expression(alert('Ping!'));"></span>
453+
$string = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#i', '$1>', $string);
454+
$string = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#i', '$1>', $string);
455+
$string = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#iu', '$1>', $string);
456+
457+
// Remove namespaced elements (we do not need them)
458+
$string = preg_replace('#</*\w+:\w[^>]*+>#i', '', $string);
459+
460+
do {
461+
// Remove really unwanted tags
462+
$oldString = $string;
463+
$string = preg_replace('#</*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:cript|tyle)|title|xml)[^>]*+>#i', '', $string);
464+
} while ($oldString !== $string);
465+
} else {
466+
// Using HTMLPurifier
467+
$config = HTMLPurifier_Config::createDefault();
468+
$purifier = new HTMLPurifier($config);
469+
$string = $purifier->purify($string);
470+
}
471+
return $string;
438472
}
473+
439474
}

framework/components/Record.php

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,8 @@ public function init(Model $beanAdapter = null, View $view = null, $isBusinessVa
239239
{
240240
$this->beanAdapter = $beanAdapter;
241241
$this->view->setVar("Separator", $this->actionsSeparator);
242-
242+
$this->view->setVar("CSRF_TOKEN_FORM_FIELD", CSRF_TOKEN_FORM_FIELD);
243+
$this->view->setVar("CRSFTOKEN", $this->getCSRFToken());
243244
// $this->doAction($beanAdapter,$isBusinessValidationError);
244245

245246
if (!empty($this->currentRecord[0])) {
@@ -276,6 +277,43 @@ public function init(Model $beanAdapter = null, View $view = null, $isBusinessVa
276277

277278
}
278279

280+
/**
281+
* Manages and gets CSRF Token
282+
* @return string The CSRF Token
283+
*/
284+
protected function getCSRFToken()
285+
{
286+
if (@empty($_SESSION['token'])) {
287+
if (function_exists('mcrypt_create_iv')) {
288+
// TODO random_bytes
289+
$_SESSION['token'] = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
290+
} else {
291+
$_SESSION['token'] = bin2hex(openssl_random_pseudo_bytes(32));
292+
}
293+
}
294+
// print_r($_SESSION['token']);
295+
return $_SESSION['token'];
296+
297+
}
298+
299+
/**
300+
* Detects CSRF attack
301+
* @return bool True if it detetecs a pibble attack.
302+
*/
303+
public function detectedCSRF()
304+
{
305+
$csrf_token_field = CSRF_TOKEN_FORM_FIELD;
306+
if (@!empty($_POST[$csrf_token_field])) {
307+
if ($_SESSION["token"] == $_POST[$csrf_token_field]) {
308+
return false;
309+
} else {
310+
return true;
311+
}
312+
} else {
313+
return true;
314+
}
315+
}
316+
279317
/**
280318
* Verifies if action's name is valid
281319
* @param string $action The action name
@@ -465,6 +503,16 @@ private function doAction(BeanAdapter $beanAdapter, $isBusinessValidationError =
465503
// Note: also excluded in observing mode.
466504
try {
467505
if (!isset($_REQUEST["getState"])) {
506+
507+
// Handles CSRF
508+
if (isset($_REQUEST[$this->record_add]) || isset($_REQUEST[$this->record_update]) || isset($_REQUEST[$this->record_delete])) {
509+
if ($this->detectedCSRF()) {
510+
$isBusinessValidationError = true;
511+
$this->addError("{RES:CRSFErrorMessage}");
512+
}
513+
}
514+
515+
// Handles ADD
468516
if (isset($_REQUEST[$this->record_add]) && !$isBusinessValidationError) {
469517
$beanAdapter->insert();
470518
if ($beanAdapter->getBean()->isSqlError()) {
@@ -474,6 +522,8 @@ private function doAction(BeanAdapter $beanAdapter, $isBusinessValidationError =
474522
if (!empty($this->redirectAfterAdd))
475523
header("Location: " . $this->redirectAfterAdd);
476524
}
525+
526+
// Handles UPDATE
477527
if (isset($_REQUEST[$this->record_update]) && !$isBusinessValidationError) {
478528
$beanAdapter->update($this->currentRecord);
479529
if ($beanAdapter->getBean()->isSqlError()) {
@@ -484,6 +534,7 @@ private function doAction(BeanAdapter $beanAdapter, $isBusinessValidationError =
484534
header("Location: " . $this->redirectAfterUpdate);
485535
}
486536

537+
// Handles DELETE
487538
if (isset($_REQUEST[$this->record_delete]) && !$isBusinessValidationError) {
488539
$beanAdapter->delete($this->currentRecord);
489540
if ($beanAdapter->getBean()->isSqlError()) {
@@ -494,6 +545,7 @@ private function doAction(BeanAdapter $beanAdapter, $isBusinessValidationError =
494545
header("Location: " . $this->redirectAfterDelete);
495546
}
496547

548+
// Handles Reset
497549
if (isset($_REQUEST[$this->record_close])) {
498550
if (!empty($this->redirectAfterClose))
499551
header("Location: " . $this->redirectAfterClose);

framework/resources/components/record.html.tpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
}
99
</style>
1010
<input type="hidden" name="Observer_Data_Changed_Alert_Message" id = "Observer_Data_Changed_Alert_Message" value="{RES:Observer_Data_Changed_Alert_Message}">
11+
<input type="hidden" name="{CSRF_TOKEN_FORM_FIELD}" value="{CRSFTOKEN}">
1112
<input class ="record_button btn btn-success" type="submit" value="{RES:Record_Add}" id="{record_add}" name="{record_add}" data-action="add">
1213
<input class ="record_button btn btn-success" type="submit" value="{RES:Record_Update}" id="{record_update}" name="{record_update}" data-action="update">
1314
<input class ="record_button btn btn-danger" type="submit" value="{RES:Record_Delete}" id="{record_delete}" name="{record_delete}" data-action="delete" data-jsconfirm="true">

0 commit comments

Comments
 (0)