Merge pull request #3444 from Icinga/bugfix/error-responding-non-html-requests-2635

Json::encode(): auto-sanitize bad UTF-8 strings
This commit is contained in:
Eric Lippmann 2018-06-22 05:01:07 -04:00 committed by GitHub
commit adfcc5596e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 148 additions and 10 deletions

View File

@ -9,6 +9,7 @@ use Icinga\Application\Logger\Writer\FileWriter;
use Icinga\Application\Logger\Writer\SyslogWriter; use Icinga\Application\Logger\Writer\SyslogWriter;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Exception\IcingaException; use Icinga\Exception\IcingaException;
use Icinga\Util\Json;
/** /**
* Logger * Logger
@ -251,7 +252,7 @@ class Logger
function ($a) { function ($a) {
return is_string($a) ? $a : ($a instanceof Exception return is_string($a) ? $a : ($a instanceof Exception
? IcingaException::describe($a) ? IcingaException::describe($a)
: json_encode($a)); : Json::encode($a));
}, },
$arguments $arguments
) )

View File

@ -22,16 +22,58 @@ class Json
* @throws JsonEncodeException * @throws JsonEncodeException
*/ */
public static function encode($value, $options = 0, $depth = 512) public static function encode($value, $options = 0, $depth = 512)
{
return static::encodeAndSanitize($value, $options, $depth, false);
}
/**
* {@link json_encode()} wrapper, automatically sanitizes bad UTF-8
*
* @param mixed $value
* @param int $options
* @param int $depth
*
* @return string
* @throws JsonEncodeException
*/
public static function sanitize($value, $options = 0, $depth = 512)
{
return static::encodeAndSanitize($value, $options, $depth, true);
}
/**
* {@link json_encode()} wrapper, sanitizes bad UTF-8
*
* @param mixed $value
* @param int $options
* @param int $depth
* @param bool $autoSanitize Automatically sanitize invalid UTF-8 (if any)
*
* @return string
* @throws JsonEncodeException
*/
protected static function encodeAndSanitize($value, $options, $depth, $autoSanitize)
{ {
if (version_compare(phpversion(), '5.5.0', '<')) { if (version_compare(phpversion(), '5.5.0', '<')) {
$encoded = json_encode($value, $options); $encoded = json_encode($value, $options);
} else { } else {
$encoded = json_encode($value, $options, $depth); $encoded = json_encode($value, $options, $depth);
} }
if (json_last_error() !== JSON_ERROR_NONE) {
switch (json_last_error()) {
case JSON_ERROR_NONE:
return $encoded;
/** @noinspection PhpMissingBreakStatementInspection */
case JSON_ERROR_UTF8:
if ($autoSanitize) {
return static::encode(static::sanitizeUtf8Recursive($value), $options, $depth);
}
// Fallthrough
default:
throw new JsonEncodeException('%s: %s', static::lastErrorMsg(), var_export($value, true)); throw new JsonEncodeException('%s: %s', static::lastErrorMsg(), var_export($value, true));
} }
return $encoded;
} }
/** /**
@ -82,4 +124,60 @@ class Json
return 'Unknown error'; return 'Unknown error';
} }
} }
/**
* Replace bad byte sequences in UTF-8 strings inside the given JSON-encodable structure with question marks
*
* @param mixed $value
*
* @return mixed
*/
protected static function sanitizeUtf8Recursive($value)
{
switch (gettype($value)) {
case 'string':
return static::sanitizeUtf8String($value);
case 'array':
$sanitized = array();
foreach ($value as $key => $val) {
if (is_string($key)) {
$key = static::sanitizeUtf8String($key);
}
$sanitized[$key] = static::sanitizeUtf8Recursive($val);
}
return $sanitized;
case 'object':
$sanitized = array();
foreach ($value as $key => $val) {
if (is_string($key)) {
$key = static::sanitizeUtf8String($key);
}
$sanitized[$key] = static::sanitizeUtf8Recursive($val);
}
return (object) $sanitized;
default:
return $value;
}
}
/**
* Replace bad byte sequences in the given UTF-8 string with question marks
*
* @param string $string
*
* @return string
*/
protected static function sanitizeUtf8String($string)
{
return mb_convert_encoding($string, 'UTF-8', 'UTF-8');
}
} }

View File

@ -3,6 +3,7 @@
namespace Icinga\Web\Announcement; namespace Icinga\Web\Announcement;
use Icinga\Util\Json;
use Icinga\Web\Cookie; use Icinga\Web\Cookie;
/** /**
@ -128,7 +129,7 @@ class AnnouncementCookie extends Cookie
*/ */
public function getValue() public function getValue()
{ {
return json_encode(array( return Json::encode(array(
'acknowledged' => $this->getAcknowledged(), 'acknowledged' => $this->getAcknowledged(),
'etag' => $this->getEtag(), 'etag' => $this->getEtag(),
'next' => $this->getNextActive() 'next' => $this->getNextActive()

View File

@ -3,6 +3,7 @@
namespace Icinga\Web\Response; namespace Icinga\Web\Response;
use Icinga\Util\Json;
use Zend_Controller_Action_HelperBroker; use Zend_Controller_Action_HelperBroker;
use Icinga\Web\Response; use Icinga\Web\Response;
@ -44,6 +45,13 @@ class JsonResponse extends Response
*/ */
protected $encodingOptions = 0; protected $encodingOptions = 0;
/**
* Whether to automatically sanitize invalid UTF-8 (if any)
*
* @var bool
*/
protected $autoSanitize = false;
/** /**
* Error message if the API call failed due to a server error * Error message if the API call failed due to a server error
* *
@ -95,6 +103,30 @@ class JsonResponse extends Response
return $this; return $this;
} }
/**
* Get whether to automatically sanitize invalid UTF-8 (if any)
*
* @return bool
*/
public function getAutoSanitize()
{
return $this->autoSanitize;
}
/**
* Set whether to automatically sanitize invalid UTF-8 (if any)
*
* @param bool $autoSanitize
*
* @return $this
*/
public function setAutoSanitize($autoSanitize = true)
{
$this->autoSanitize = $autoSanitize;
return $this;
}
/** /**
* Get the error message if the API call failed due to a server error * Get the error message if the API call failed due to a server error
* *
@ -190,7 +222,9 @@ class JsonResponse extends Response
$body['data'] = $this->getSuccessData(); $body['data'] = $this->getSuccessData();
break; break;
} }
echo json_encode($body, $this->getEncodingOptions()); echo $this->getAutoSanitize()
? Json::sanitize($body, $this->getEncodingOptions())
: Json::encode($body, $this->getEncodingOptions());
} }
/** /**

View File

@ -10,6 +10,7 @@ use Icinga\Cli\Command;
use Icinga\File\Csv; use Icinga\File\Csv;
use Icinga\Module\Monitoring\Plugin\PerfdataSet; use Icinga\Module\Monitoring\Plugin\PerfdataSet;
use Exception; use Exception;
use Icinga\Util\Json;
/** /**
* Icinga monitoring objects * Icinga monitoring objects
@ -78,7 +79,7 @@ class ListCommand extends Command
$query = $query->getQuery(); $query = $query->getQuery();
switch ($format) { switch ($format) {
case 'json': case 'json':
echo json_encode($query->fetchAll()); echo Json::sanitize($query->fetchAll());
break; break;
case 'csv': case 'csv':
Csv::fromQuery($query)->dump(); Csv::fromQuery($query)->dump();

View File

@ -60,7 +60,7 @@ class Controller extends IcingaWebController
'Content-Disposition', 'Content-Disposition',
'inline; filename=' . $this->getRequest()->getActionName() . '.json' 'inline; filename=' . $this->getRequest()->getActionName() . '.json'
) )
->appendBody(Json::encode($query->fetchAll())) ->appendBody(Json::sanitize($query->fetchAll()))
->sendResponse(); ->sendResponse();
exit; exit;
case 'csv': case 'csv':

View File

@ -153,7 +153,10 @@ abstract class MonitoredObjectController extends Controller
); );
$groupName = $this->object->getType() . 'groups'; $groupName = $this->object->getType() . 'groups';
$payload[$groupName] = $this->object->$groupName; $payload[$groupName] = $this->object->$groupName;
$this->getResponse()->json()->setSuccessData($payload)->sendResponse(); $this->getResponse()->json()
->setSuccessData($payload)
->setAutoSanitize()
->sendResponse();
} }
} }

View File

@ -173,7 +173,7 @@ class RestRequest
{ {
switch ($contentType) { switch ($contentType) {
case 'application/json': case 'application/json':
$payload = json_encode($payload); $payload = Json::encode($payload);
break; break;
} }