Skip to content
This repository has been archived by the owner on Dec 13, 2022. It is now read-only.

fix(comments): fix XSS vulnerability on hosts and services comments #6953

Merged
merged 8 commits into from
Dec 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions behat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,7 @@ default:
topcounterpoller:
paths: [ %paths.base%/features/TopCounterPollers.feature ]
contexts: [ TopCounterPollersContext ]

vulnerability_comment:
paths: [ %paths.base%/features/VulnerabilityComment.feature ]
contexts: [ VulnerabilityCommentContext ]
20 changes: 20 additions & 0 deletions features/VulnerabilityComment.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Feature: Check XSS Vulnerability on comments
As a Centreon user
I want to comment a service and host
To check XSS vulnerability

Background:
Given I am logged in a Centreon server
And an host is configured
And a service is configured

@critical
Scenario: Check XSS vulnerability on comment host
When I add a comment on host
Then The html is not interpreted on comments in host details

@critical
Scenario: Check XSS vulnerability on comment service
When I add a comment on service
Then The html is not interpreted in general list of comments
And The html is not interpreted on comments in service details
175 changes: 175 additions & 0 deletions features/bootstrap/VulnerabilityCommentContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

use Centreon\Test\Behat\CentreonContext;
use Centreon\Test\Behat\Configuration\HostConfigurationPage;
use Centreon\Test\Behat\Configuration\ServiceConfigurationPage;
use Centreon\Test\Behat\Configuration\CommentConfigurationPage;
use Centreon\Test\Behat\Configuration\CommentHostConfigurationPage;
use Centreon\Test\Behat\Configuration\CommentListingPage;
use Centreon\Test\Behat\Monitoring\ServiceMonitoringDetailsPage;
use Centreon\Test\Behat\Monitoring\HostMonitoringDetailsPage;
use Centreon\Test\Behat\Exception\ClosureException;

class VulnerabilityCommentContext extends CentreonContext
{
const HOSTNAME = 'hostName';
const SERVICE_DESCRIPTION = 'serviceDescription';
protected $currentPage;
protected $hostName = "hostname";

protected $hostProperties = array(
'name' => self::HOSTNAME,
'alias' => 'hostAlias',
'address' => 'host2@localhost'
);

private $labelButton = 'test button';

protected $commentProperties;

public function __construct(array $parameters = array())
{
parent::__construct($parameters);
$this->commentProperties = [
'comment' => '<button>' . $this->labelButton . '</button>'
];
}

protected $serviceProperties = array(
'hosts' => self::HOSTNAME,
'description' => self::SERVICE_DESCRIPTION,
'templates' => 'generic-service',
'check_command' => 'check_centreon_dummy',
'check_period' => '24x7',
'max_check_attempts' => 1,
'normal_check_interval' => 1,
'retry_check_interval' => 1,
'active_checks_enabled' => 1,
'passive_checks_enabled' => 0,
'notifications_enabled' => 1,
'notify_on_recovery' => 1,
'notify_on_critical' => 1,
'recovery_notification_delay' => 1,
'cs' => 'admin_admin'
);

/**
* @Given an host is configured
*/
public function anHostIsConfigured()
{
$currentPage = new HostConfigurationPage($this);
$currentPage->setProperties($this->hostProperties);
$currentPage->save();
}

/**
* @Given a service is configured
*/
public function aServiceIsConfigured()
{
$currentPage = new ServiceConfigurationPage($this);
$currentPage->setProperties($this->serviceProperties);
$currentPage->save();
}

/**
* @When I add a comment on service
*/
public function iAddACommentOnService() {
$this->reloadAllPollers();
$currentPage = new CommentConfigurationPage(
$this,
self::HOSTNAME,
self::SERVICE_DESCRIPTION
);
$currentPage->setProperties($this->commentProperties);
$currentPage->save();
}

/**
* @When I add a comment on host
*/
public function iAddACommentOnHost() {
$this->reloadAllPollers();
$currentPage = new CommentHostConfigurationPage(
$this,
self::HOSTNAME
);
$currentPage->setProperties($this->commentProperties);
$currentPage->save();
}

/**
* @Then The html is not interpreted in general list of comments
*/
public function theHtmlIsNotInterpretedInGeneralListOfComments() {
$this->spin(
function($context) {
$currentPage = new CommentListingPage($context, true);
$comments = $currentPage->getEntries();
if (!empty($comments)) {
$comment = array_shift($comments);
if ($comment['comment'] === $this->commentProperties['comment']) {
return true;
} else {
throw new ClosureException("XSS vulnerability detected");
}
}
return false;
}
);
}

/**
* @Then The html is not interpreted on comments in service details
*/
public function theHtmlIsNotInterpretedOnCommentsInServiceDetails() {
$this->spin(
function($context) {
$currentPage = new ServiceMonitoringDetailsPage(
$context,
self::HOSTNAME,
self::SERVICE_DESCRIPTION
);
$comments = $currentPage->getComments();
if (!empty($comments)) {
$comment = array_shift($comments);
if ($comment['comment'] === $this->commentProperties['comment']) {
return true;
} else {
throw new ClosureException("XSS vulnerability detected");
}
}
return false;
}
);
}

/**
* @Then The html is not interpreted on comments in host details
*/
public function theHtmlIsNotInterpretedOnCommentsInHostDetails() {
$this->spin(
function($context) {
$currentPage = new HostMonitoringDetailsPage(
$context,
self::HOSTNAME
);
$currentPage->switchTab(4);
$comments = $currentPage->getComments();
if (!empty($comments)) {
$comment = array_shift($comments);
if ($comment['comment'] === $this->commentProperties['comment']) {
return true;
} else {
throw new ClosureException("XSS vulnerability detected");
}
}
return false;
}
);
}


}
167 changes: 160 additions & 7 deletions www/class/centreonUtils.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@

class CentreonUtils
{
/**
* Remove all <script> data
*/
const ESCAPE_LEGACY_METHOD = 0;
/**
* Convert all html tags into HTML entities except links
*/
const ESCAPE_ALL_EXCEPT_LINK = 1;
/**
* Convert all html tags into HTML entities
*/
const ESCAPE_ALL = 2;

/**
* Defines all self-closing html tags allowed
*/
public static $selfclosingHtmlTagsAllowed = array('br', 'hr');

/**
* Converts Object into Array
*
Expand Down Expand Up @@ -295,17 +313,152 @@ public static function compareVersion($currentVersion, $targetVersion, $delimite
}

/**
* Escape a string for present javascript injection
* Convert an HTML string according to the selected method
*
* @param string $string The string to escape
* @return string
* @param string $stringToEscape String to escape
* @param int $escapeMethod Escape method (default: ESCAPE_LEGACY_METHOD)
* @return string Escaped string
* @see CentreonUtils::ESCAPE_LEGACY_METHOD
* @see CentreonUtils::ESCAPE_ALL_EXCEPT_LINK
* @see CentreonUtils::ESCAPE_ALL
*/
public static function escapeSecure(
$stringToEscape,
$escapeMethod = self::ESCAPE_LEGACY_METHOD
) {
switch ($escapeMethod) {
case self::ESCAPE_LEGACY_METHOD:
return preg_replace("/<script.*?\/script>/s", "", $stringToEscape);
case self::ESCAPE_ALL_EXCEPT_LINK:
return self::escapeAllExceptLink($stringToEscape);
case self::ESCAPE_ALL:
return self::escapeAll($stringToEscape);
}
}

/**
* Convert all html tags into HTML entities
*
* @param type $stringToEscape String to escape
* @return string Converted string
*/
public static function escapeSecure($string)
public static function escapeAll($stringToEscape)
{
/* Remove script tags */
$string = preg_replace("/<script.*?\/script>/s", "", $string);
return htmlentities($stringToEscape, ENT_QUOTES, 'UTF-8');
}

return $string;
/**
* Convert all HTML tags into HTML entities except those defined in parameter
*
* @param string $stringToEscape String (HTML) to escape
* @param string[] $tagsNotToEscape List of tags not to escape
* @return string HTML escaped
*/
public static function escapeAllExceptSelectedTags(
$stringToEscape,
$tagsNotToEscape = array()
) {
if (!is_array($tagsNotToEscape)) {
// Do nothing if the tag list is empty
return $stringToEscape;
}

$tagOccurences = array();
/**
* Before to escape HTML, we will search and replace all HTML tags
* allowed by specific tags to avoid they are processed
*/
for ($indexTag = 0; $indexTag < count($tagsNotToEscape); $indexTag++) {
$linkToken = "{{__TAG{$indexTag}x__}}";
$currentTag = $tagsNotToEscape[$indexTag];
if (!in_array($currentTag, self::$selfclosingHtmlTagsAllowed)) {
// The current tag is not self-closing tag allowed
$index = 0;
$tagsFound = array();

// Specific process for not self-closing HTML tags
while ($occurence = self::getHtmlTags($currentTag, $stringToEscape)) {
$tagsFound[$index] = $occurence['tag'];
$linkTag = str_replace('x', $index, $linkToken);
$stringToEscape = substr_replace(
$stringToEscape,
$linkTag,
$occurence['start'],
$occurence['length']
);
$index++;
}
} else {
$linkToken = '{{__' . strtoupper($currentTag) . '__}}';
// Specific process for self-closing tag
$stringToEscape = preg_replace(
'~< *(' . $currentTag . ')+ *\/?>~im',
$linkToken,
$stringToEscape
);
$tagsFound = array("<$currentTag/>");
}
$tagOccurences[$linkToken] = $tagsFound;
}

$escapedString = htmlentities($stringToEscape, ENT_QUOTES, 'UTF-8');

/**
* After we escaped all unauthorized HTML tags, we will search and
* replace all previous specifics tags by their original tag
*/
foreach ($tagOccurences as $linkToken => $tagsFound) {
for ($indexTag = 0; $indexTag < count($tagsFound); $indexTag++) {
$linkTag = str_replace('x', $indexTag, $linkToken);
$escapedString = str_replace($linkTag, $tagsFound[$indexTag], $escapedString);
}
}

return $escapedString;
}

/**
* Convert all html tags into HTML entities except links (<a>...</a>)
*
* @param string $stringToEscape String (HTML) to escape
* @return string HTML escaped (except links)
*/
public static function escapeAllExceptLink($stringToEscape)
{
return self::escapeAllExceptSelectedTags($stringToEscape, array('a'));
}

/**
* Return all occurences of a html tag found in html string
*
* @param string $tag HTML tag to find
* @param string $html HTML to analyse
* @return array (('tag'=> html tag; 'start' => start position of tag,
* 'length'=> length between start and end of tag), ...)
*/
public static function getHtmlTags($tag, $html)
{
$occurrences = false;
$start = 0;
$end = 0;
if (($start = stripos($html, "<$tag", $start)) !== false &&
($end = stripos($html, "</$tag>", $end + strlen("</$tag>")))
) {
if (!is_array($occurrences[$tag])) {
$occurrences[$tag] = array();
}
$occurrences =
array(
'tag' => substr(
$html,
$start,
$end + strlen("</$tag>") - $start
),
'start' => $start,
'length' => $end + strlen("</$tag>") - $start
);
}
return $occurrences;
}

/**
Expand Down