Skip to content

Commit

Permalink
Create new RequestSanitizer service that exists in DI. Moved sanitiza…
Browse files Browse the repository at this point in the history
…tion logic in Common.php methods to this class.
  • Loading branch information
diosmosis committed Jun 2, 2015
1 parent 1664da1 commit 86558fe
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 119 deletions.
129 changes: 16 additions & 113 deletions core/Common.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use Exception;
use Piwik\Container\StaticContainer;
use Piwik\Http\RequestSanitizer;
use Piwik\Intl\Data\Provider\LanguageDataProvider;
use Piwik\Intl\Data\Provider\RegionDataProvider;
use Piwik\Plugins\UserCountry\LocationProvider\DefaultProvider;
Expand All @@ -28,9 +29,6 @@ class Common
const REFERRER_TYPE_WEBSITE = 3;
const REFERRER_TYPE_CAMPAIGN = 6;

// Flag used with htmlspecialchar. See php.net/htmlspecialchars.
const HTML_ENCODING_QUOTE_STYLE = ENT_QUOTES;

public static $isCliMode = null;

/*
Expand Down Expand Up @@ -263,33 +261,9 @@ public static function mb_strtolower($string)
*/
public static function sanitizeInputValues($value, $alreadyStripslashed = false)
{
if (is_numeric($value)) {
return $value;
} elseif (is_string($value)) {
$value = self::sanitizeString($value);

if (!$alreadyStripslashed) {
// a JSON array was already stripslashed, don't do it again for each value

$value = self::undoMagicQuotes($value);
}
} elseif (is_array($value)) {
foreach (array_keys($value) as $key) {
$newKey = $key;
$newKey = self::sanitizeInputValues($newKey, $alreadyStripslashed);
if ($key != $newKey) {
$value[$newKey] = $value[$key];
unset($value[$key]);
}

$value[$newKey] = self::sanitizeInputValues($value[$newKey], $alreadyStripslashed);
}
} elseif (!is_null($value)
&& !is_bool($value)
) {
throw new Exception("The value to escape has not a supported type. Value = " . var_export($value, true));
}
return $value;
/** @var RequestSanitizer $requestSanitizer */
$requestSanitizer = StaticContainer::get('Piwik\Http\RequestSanitizer');
return $requestSanitizer->sanitizeValues($value, $alreadyStripslashed);
}

/**
Expand All @@ -300,37 +274,9 @@ public static function sanitizeInputValues($value, $alreadyStripslashed = false)
*/
public static function sanitizeInputValue($value)
{
$value = self::sanitizeLineBreaks($value);
$value = self::sanitizeString($value);
return $value;
}

/**
* Sanitize a single input value
*
* @param $value
* @return string
*/
private static function sanitizeString($value)
{
// $_GET and $_REQUEST already urldecode()'d
// decode
// note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items
$value = html_entity_decode($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');

$value = self::sanitizeNullBytes($value);

// escape
$tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');

// note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8
if ($value != '' && $tmp == '') {
// convert and escape
$value = utf8_encode($value);
$tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
return $tmp;
}
return $tmp;
/** @var RequestSanitizer $requestSanitizer */
$requestSanitizer = StaticContainer::get('Piwik\Http\RequestSanitizer');
return $requestSanitizer->sanitizeValue($value);
}

/**
Expand All @@ -341,7 +287,9 @@ private static function sanitizeString($value)
*/
public static function unsanitizeInputValue($value)
{
return htmlspecialchars_decode($value, self::HTML_ENCODING_QUOTE_STYLE);
/** @var RequestSanitizer $requestSanitizer */
$requestSanitizer = StaticContainer::get('Piwik\Http\RequestSanitizer');
return $requestSanitizer->unsanitizeValue($value);
}

/**
Expand All @@ -360,54 +308,9 @@ public static function unsanitizeInputValue($value)
*/
public static function unsanitizeInputValues($value)
{
if (is_array($value)) {
$result = array();
foreach ($value as $key => $arrayValue) {
$result[$key] = self::unsanitizeInputValues($arrayValue);
}
return $result;
} else {
return self::unsanitizeInputValue($value);
}
}

/**
* Undo the damage caused by magic_quotes; deprecated in php 5.3 but not removed until php 5.4
*
* @param string
* @return string modified or not
*/
private static function undoMagicQuotes($value)
{
static $shouldUndo;

if (!isset($shouldUndo)) {
$shouldUndo = version_compare(PHP_VERSION, '5.4', '<') && get_magic_quotes_gpc();
}

if ($shouldUndo) {
$value = stripslashes($value);
}

return $value;
}

/**
* @param string $value
* @return string Line breaks and line carriage removed
*/
public static function sanitizeLineBreaks($value)
{
return str_replace(array("\n", "\r"), '', $value);
}

/**
* @param string $value
* @return string Null bytes removed
*/
public static function sanitizeNullBytes($value)
{
return str_replace(array("\0"), '', $value);
/** @var RequestSanitizer $requestSanitizer */
$requestSanitizer = StaticContainer::get('Piwik\Http\RequestSanitizer');
return $requestSanitizer->unsanitizeValues($value);
}

/**
Expand Down Expand Up @@ -468,9 +371,9 @@ public static function getRequestVar($varName, $varDefault = null, $varType = nu

// we deal w/ json differently
if ($varType == 'json') {
$value = self::undoMagicQuotes($requestArrayToUse[$varName]);
$value = json_decode($value, $assoc = true);
return self::sanitizeInputValues($value, $alreadyStripslashed = true);
/** @var RequestSanitizer $requestSanitizer */
$requestSanitizer = StaticContainer::get('Piwik\Http\RequestSanitizer');
return $requestSanitizer->sanitizeJsonValues($requestArrayToUse[$varName]);
}

$value = self::sanitizeInputValues($requestArrayToUse[$varName]);
Expand Down
4 changes: 3 additions & 1 deletion core/ExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use Exception;
use Piwik\Container\ContainerDoesNotExistException;
use Piwik\Http\RequestSanitizer;
use Piwik\Plugins\CoreAdminHome\CustomLogo;

/**
Expand Down Expand Up @@ -68,7 +69,8 @@ private static function getErrorResponse(Exception $ex)
$message = $ex->getMessage();

if (!method_exists($ex, 'isHtmlMessage') || !$ex->isHtmlMessage()) {
$message = Common::sanitizeInputValue($message);
$sanitizer = new RequestSanitizer();
$message = $sanitizer->sanitizeValue($message);
}

$logo = new CustomLogo();
Expand Down
189 changes: 189 additions & 0 deletions core/Http/RequestSanitizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Http;

/**
* Service that sanitizes query parameters for requests.
*
* Eventually once consistent out-only sanitization is implemented, this class should be
* removed. In the mean time, since this class is accessed through DI, it can be used
* to implement consistent sanitization, piece by piece.
*/
class RequestSanitizer
{
// Flag used with htmlspecialchar. See php.net/htmlspecialchars.
const HTML_ENCODING_QUOTE_STYLE = ENT_QUOTES;

/**
* Sanitizes a single value.
*
* @param mixed $value If numeric, the value is not changed. If a string, line breaks are removed and HTML entities
* are escaped.
* @param bool $alreadyStripslashed Implementation detail, ignore.
* @return string
* @throws \Exception If the value is null or a boolean.
*/
public function sanitizeValue($value, $alreadyStripslashed = false)
{
if (is_numeric($value)) {
return $value;
} else if (is_string($value)) {
$value = $this->sanitizeLineBreaks($value);
$value = $this->sanitizeString($value);

if (!$alreadyStripslashed) { // a JSON array was already stripslashed, don't do it again for each value
$value = $this->undoMagicQuotes($value);
}
} else if (!is_null($value)
&& !is_bool($value)
) {
throw new \Exception("The value to escape has not a supported type. Value = " . var_export($value, true));
}
return $value;
}

/**
* Unsanitizes a single value. Replaces escaped HTML entities with the actual characters.
*
* @param string $value
* @return string
*/
public function unsanitizeValue($value)
{
return htmlspecialchars_decode($value, self::HTML_ENCODING_QUOTE_STYLE);
}

/**
* Sanitize an array of values recursively.
*
* @param mixed $value If an array, it is sanitized recursively. Keys are sanitized as well as values. If not,
* it is sanitized normally.
* @param bool $alreadyStripslashed Implementation detail, ignore.
* @return mixed
*/
public function sanitizeValues($value, $alreadyStripslashed = false)
{
if (is_array($value)) {
foreach (array_keys($value) as $key) {
$newKey = $this->sanitizeValue($key, $alreadyStripslashed);

if ($key != $newKey) {
$value[$newKey] = $value[$key];
unset($value[$key]);
}

$value[$newKey] = $this->sanitizeValues($value[$newKey], $alreadyStripslashed);
}
return $value;
} else {
return $this->sanitizeValue($value);
}
}

/**
* Unsanitize array values recursively.
*
* @param mixed $values If an array, it is unsanitized recursively. Keys are not unsanitized. If not an array,
* it is sanitized normally.
* @return mixed
*/
public function unsanitizeValues($value)
{
if (is_array($value)) {
$result = array();
foreach ($value as $key => $arrayValue) {
$result[$key] = $this->unsanitizeValues($arrayValue);
}
return $result;
} else {
return $this->unsanitizeValue($value);
}
}

/**
* @param string $value
* @return string Line breaks and line carriage removed
*/
public function sanitizeLineBreaks($value)
{
return str_replace(array("\n", "\r"), '', $value);
}

/**
* Sanitizes a JSON string by value.
*
* @param string $value Parses a JSON string into an array and then sanitizes every element recursively. Array
* keys are sanitized as well.
* @return array
*/
public function sanitizeJsonValues($value)
{
$value = $this->undoMagicQuotes($value);
$value = json_decode($value, $assoc = true);
return $this->sanitizeValues($value, $alreadyStripslashed = true);
}

/**
* Sanitize a single input value
*
* @param $value
* @return string
*/
private function sanitizeString($value)
{
// $_GET and $_REQUEST already urldecode()'d
// decode
// note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items
$value = html_entity_decode($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');

$value = $this->sanitizeNullBytes($value);

// escape
$tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');

// note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8
if ($value != '' && $tmp == '') {
// convert and escape
$value = utf8_encode($value);
$tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8');
return $tmp;
}
return $tmp;
}

/**
* @param string $value
* @return string Null bytes removed
*/
private function sanitizeNullBytes($value)
{
return str_replace(array("\0"), '', $value);
}

/**
* Undo the damage caused by magic_quotes; deprecated in php 5.3 but not removed until php 5.4
*
* @param string
* @return string modified or not
*/
private function undoMagicQuotes($value)
{
static $shouldUndo;

if (!isset($shouldUndo)) {
$shouldUndo = version_compare(PHP_VERSION, '5.4', '<') && get_magic_quotes_gpc();
}

if ($shouldUndo) {
$value = stripslashes($value);
}

return $value;
}
}
Loading

0 comments on commit 86558fe

Please sign in to comment.