/var/www/vhosts/ihelp.ro/_OLD/vendor/cakephp/cakephp/src/Controller/Component
Edit: /var/www/vhosts/ihelp.ro/_OLD/vendor/cakephp/cakephp/src/Controller/Component/SecurityComponent.php (20594B)
null,
'requireSecure' => [],
'unlockedFields' => [],
'unlockedActions' => [],
'validatePost' => true,
];
/**
* Holds the current action of the controller
*
* @var string
*/
protected $_action;
/**
* Component startup. All security checking happens here.
*
* @param \Cake\Event\EventInterface $event An Event instance
* @return \Cake\Http\Response|null
*/
public function startup(EventInterface $event): ?Response
{
/** @var \Cake\Controller\Controller $controller */
$controller = $event->getSubject();
$request = $controller->getRequest();
$this->_action = $request->getParam('action');
$hasData = ($request->getData() || $request->is(['put', 'post', 'delete', 'patch']));
try {
$this->_secureRequired($controller);
if ($this->_action === $this->_config['blackHoleCallback']) {
throw new AuthSecurityException(sprintf(
'Action %s is defined as the blackhole callback.',
$this->_action
));
}
if (
!in_array($this->_action, (array)$this->_config['unlockedActions'], true) &&
$hasData &&
$this->_config['validatePost']
) {
$this->_validatePost($controller);
}
} catch (SecurityException $se) {
return $this->blackHole($controller, $se->getType(), $se);
}
$request = $this->generateToken($request);
if ($hasData && is_array($controller->getRequest()->getData())) {
$request = $request->withoutData('_Token');
}
$controller->setRequest($request);
return null;
}
/**
* Events supported by this component.
*
* @return array
*/
public function implementedEvents(): array
{
return [
'Controller.startup' => 'startup',
];
}
/**
* Sets the actions that require a request that is SSL-secured, or empty for all actions
*
* @param string|string[]|null $actions Actions list
* @return void
*/
public function requireSecure($actions = null): void
{
$actions = (array)$actions;
$this->setConfig('requireSecure', empty($actions) ? ['*'] : $actions);
}
/**
* Black-hole an invalid request with a 400 error or custom callback. If SecurityComponent::$blackHoleCallback
* is specified, it will use this callback by executing the method indicated in $error
*
* @param \Cake\Controller\Controller $controller Instantiating controller
* @param string $error Error method
* @param \Cake\Controller\Exception\SecurityException|null $exception Additional debug info describing the cause
* @return mixed If specified, controller blackHoleCallback's response, or no return otherwise
* @see \Cake\Controller\Component\SecurityComponent::$blackHoleCallback
* @link https://book.cakephp.org/4/en/controllers/components/security.html#handling-blackhole-callbacks
* @throws \Cake\Http\Exception\BadRequestException
*/
public function blackHole(Controller $controller, string $error = '', ?SecurityException $exception = null)
{
if (!$this->_config['blackHoleCallback']) {
$this->_throwException($exception);
}
return $this->_callback($controller, $this->_config['blackHoleCallback'], [$error, $exception]);
}
/**
* Check debug status and throw an Exception based on the existing one
*
* @param \Cake\Controller\Exception\SecurityException|null $exception Additional debug info describing the cause
* @throws \Cake\Http\Exception\BadRequestException
* @return void
*/
protected function _throwException(?SecurityException $exception = null): void
{
if ($exception !== null) {
if (!Configure::read('debug')) {
$exception->setReason($exception->getMessage());
$exception->setMessage(static::DEFAULT_EXCEPTION_MESSAGE);
}
throw $exception;
}
throw new BadRequestException(static::DEFAULT_EXCEPTION_MESSAGE);
}
/**
* Check if access requires secure connection
*
* @param \Cake\Controller\Controller $controller Instantiating controller
* @return void
* @throws \Cake\Controller\Exception\SecurityException
*/
protected function _secureRequired(Controller $controller): void
{
if (
empty($this->_config['requireSecure']) ||
!is_array($this->_config['requireSecure'])
) {
return;
}
$requireSecure = $this->_config['requireSecure'];
if (
($requireSecure[0] === '*' ||
in_array($this->_action, $requireSecure, true)
) &&
!$controller->getRequest()->is('ssl')
) {
throw new SecurityException(
'Request is not SSL and the action is required to be secure'
);
}
}
/**
* Validate submitted form
*
* @param \Cake\Controller\Controller $controller Instantiating controller
* @return void
* @throws \Cake\Controller\Exception\AuthSecurityException
*/
protected function _validatePost(Controller $controller): void
{
$token = $this->_validToken($controller);
$hashParts = $this->_hashParts($controller);
$check = hash_hmac('sha1', implode('', $hashParts), Security::getSalt());
if (hash_equals($check, $token)) {
return;
}
$msg = static::DEFAULT_EXCEPTION_MESSAGE;
if (Configure::read('debug')) {
$msg = $this->_debugPostTokenNotMatching($controller, $hashParts);
}
throw new AuthSecurityException($msg);
}
/**
* Check if token is valid
*
* @param \Cake\Controller\Controller $controller Instantiating controller
* @throws \Cake\Controller\Exception\SecurityException
* @return string fields token
*/
protected function _validToken(Controller $controller): string
{
$check = $controller->getRequest()->getData();
$message = '\'%s\' was not found in request data.';
if (!isset($check['_Token'])) {
throw new AuthSecurityException(sprintf($message, '_Token'));
}
if (!isset($check['_Token']['fields'])) {
throw new AuthSecurityException(sprintf($message, '_Token.fields'));
}
if (!is_string($check['_Token']['fields'])) {
throw new AuthSecurityException("'_Token.fields' is invalid.");
}
if (!isset($check['_Token']['unlocked'])) {
throw new AuthSecurityException(sprintf($message, '_Token.unlocked'));
}
if (Configure::read('debug') && !isset($check['_Token']['debug'])) {
throw new SecurityException(sprintf($message, '_Token.debug'));
}
if (!Configure::read('debug') && isset($check['_Token']['debug'])) {
throw new SecurityException('Unexpected \'_Token.debug\' found in request data');
}
$token = urldecode($check['_Token']['fields']);
if (strpos($token, ':')) {
[$token, ] = explode(':', $token, 2);
}
return $token;
}
/**
* Return hash parts for the Token generation
*
* @param \Cake\Controller\Controller $controller Instantiating controller
* @return string[]
*/
protected function _hashParts(Controller $controller): array
{
$request = $controller->getRequest();
// Start the session to ensure we get the correct session id.
$session = $request->getSession();
$session->start();
$data = (array)$request->getData();
$fieldList = $this->_fieldsList($data);
$unlocked = $this->_sortedUnlocked($data);
return [
Router::url($request->getRequestTarget()),
serialize($fieldList),
$unlocked,
$session->id(),
];
}
/**
* Return the fields list for the hash calculation
*
* @param array $check Data array
* @return array
*/
protected function _fieldsList(array $check): array
{
$locked = '';
$token = urldecode($check['_Token']['fields']);
$unlocked = $this->_unlocked($check);
if (strpos($token, ':')) {
[, $locked] = explode(':', $token, 2);
}
unset($check['_Token']);
$locked = $locked ? explode('|', $locked) : [];
$unlocked = $unlocked ? explode('|', $unlocked) : [];
$fields = Hash::flatten($check);
$fieldList = array_keys($fields);
$multi = $lockedFields = [];
$isUnlocked = false;
foreach ($fieldList as $i => $key) {
if (is_string($key) && preg_match('/(\.\d+){1,10}$/', $key)) {
$multi[$i] = preg_replace('/(\.\d+){1,10}$/', '', $key);
unset($fieldList[$i]);
} else {
$fieldList[$i] = (string)$key;
}
}
if (!empty($multi)) {
$fieldList += array_unique($multi);
}
$unlockedFields = array_unique(
array_merge(
(array)$this->_config['unlockedFields'],
$unlocked
)
);
foreach ($fieldList as $i => $key) {
$isLocked = in_array($key, $locked, true);
if (!empty($unlockedFields)) {
foreach ($unlockedFields as $off) {
$off = explode('.', $off);
$field = array_values(array_intersect(explode('.', $key), $off));
$isUnlocked = ($field === $off);
if ($isUnlocked) {
break;
}
}
}
if ($isUnlocked || $isLocked) {
unset($fieldList[$i]);
if ($isLocked) {
$lockedFields[$key] = $fields[$key];
}
}
}
sort($fieldList, SORT_STRING);
ksort($lockedFields, SORT_STRING);
$fieldList += $lockedFields;
return $fieldList;
}
/**
* Get the unlocked string
*
* @param array $data Data array
* @return string
*/
protected function _unlocked(array $data): string
{
return urldecode($data['_Token']['unlocked']);
}
/**
* Get the sorted unlocked string
*
* @param array $data Data array
* @return string
*/
protected function _sortedUnlocked(array $data): string
{
$unlocked = $this->_unlocked($data);
$unlocked = explode('|', $unlocked);
sort($unlocked, SORT_STRING);
return implode('|', $unlocked);
}
/**
* Create a message for humans to understand why Security token is not matching
*
* @param \Cake\Controller\Controller $controller Instantiating controller
* @param array $hashParts Elements used to generate the Token hash
* @return string Message explaining why the tokens are not matching
*/
protected function _debugPostTokenNotMatching(Controller $controller, array $hashParts): string
{
$messages = [];
$expectedParts = json_decode(urldecode($controller->getRequest()->getData('_Token.debug')), true);
if (!is_array($expectedParts) || count($expectedParts) !== 3) {
return 'Invalid security debug token.';
}
$expectedUrl = Hash::get($expectedParts, 0);
$url = Hash::get($hashParts, 0);
if ($expectedUrl !== $url) {
$messages[] = sprintf('URL mismatch in POST data (expected \'%s\' but found \'%s\')', $expectedUrl, $url);
}
$expectedFields = Hash::get($expectedParts, 1);
$dataFields = Hash::get($hashParts, 1);
if ($dataFields) {
$dataFields = unserialize($dataFields);
}
$fieldsMessages = $this->_debugCheckFields(
$dataFields,
$expectedFields,
'Unexpected field \'%s\' in POST data',
'Tampered field \'%s\' in POST data (expected value \'%s\' but found \'%s\')',
'Missing field \'%s\' in POST data'
);
$expectedUnlockedFields = Hash::get($expectedParts, 2);
$dataUnlockedFields = Hash::get($hashParts, 2) ?: null;
if ($dataUnlockedFields) {
$dataUnlockedFields = explode('|', $dataUnlockedFields);
}
$unlockFieldsMessages = $this->_debugCheckFields(
(array)$dataUnlockedFields,
$expectedUnlockedFields,
'Unexpected unlocked field \'%s\' in POST data',
'',
'Missing unlocked field: \'%s\''
);
$messages = array_merge($messages, $fieldsMessages, $unlockFieldsMessages);
return implode(', ', $messages);
}
/**
* Iterates data array to check against expected
*
* @param array $dataFields Fields array, containing the POST data fields
* @param array $expectedFields Fields array, containing the expected fields we should have in POST
* @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
* @param string $stringKeyMessage Message string if tampered found in
* data fields indexed by string (protected).
* @param string $missingMessage Message string if missing field
* @return string[] Messages
*/
protected function _debugCheckFields(
array $dataFields,
array $expectedFields = [],
string $intKeyMessage = '',
string $stringKeyMessage = '',
string $missingMessage = ''
): array {
$messages = $this->_matchExistingFields($dataFields, $expectedFields, $intKeyMessage, $stringKeyMessage);
$expectedFieldsMessage = $this->_debugExpectedFields($expectedFields, $missingMessage);
if ($expectedFieldsMessage !== null) {
$messages[] = $expectedFieldsMessage;
}
return $messages;
}
/**
* Manually add form tampering prevention token information into the provided
* request object.
*
* @param \Cake\Http\ServerRequest $request The request object to add into.
* @return \Cake\Http\ServerRequest The modified request.
*/
public function generateToken(ServerRequest $request): ServerRequest
{
$token = [
'unlockedFields' => $this->_config['unlockedFields'],
];
return $request->withAttribute('formTokenData', [
'unlockedFields' => $token['unlockedFields'],
]);
}
/**
* Calls a controller callback method
*
* @param \Cake\Controller\Controller $controller Instantiating controller
* @param string $method Method to execute
* @param array $params Parameters to send to method
* @return mixed Controller callback method's response
* @throws \Cake\Http\Exception\BadRequestException When a the blackholeCallback is not callable.
*/
protected function _callback(Controller $controller, string $method, array $params = [])
{
$callable = [$controller, $method];
if (!is_callable($callable)) {
throw new BadRequestException('The request has been black-holed');
}
return $callable(...$params);
}
/**
* Generate array of messages for the existing fields in POST data, matching dataFields in $expectedFields
* will be unset
*
* @param array $dataFields Fields array, containing the POST data fields
* @param array $expectedFields Fields array, containing the expected fields we should have in POST
* @param string $intKeyMessage Message string if unexpected found in data fields indexed by int (not protected)
* @param string $stringKeyMessage Message string if tampered found in
* data fields indexed by string (protected)
* @return string[] Error messages
*/
protected function _matchExistingFields(
array $dataFields,
array &$expectedFields,
string $intKeyMessage,
string $stringKeyMessage
): array {
$messages = [];
foreach ($dataFields as $key => $value) {
if (is_int($key)) {
$foundKey = array_search($value, (array)$expectedFields, true);
if ($foundKey === false) {
$messages[] = sprintf($intKeyMessage, $value);
} else {
unset($expectedFields[$foundKey]);
}
} else {
if (isset($expectedFields[$key]) && $value !== $expectedFields[$key]) {
$messages[] = sprintf($stringKeyMessage, $key, $expectedFields[$key], $value);
}
unset($expectedFields[$key]);
}
}
return $messages;
}
/**
* Generate debug message for the expected fields
*
* @param array $expectedFields Expected fields
* @param string $missingMessage Message template
* @return string|null Error message about expected fields
*/
protected function _debugExpectedFields(array $expectedFields = [], string $missingMessage = ''): ?string
{
if (count($expectedFields) === 0) {
return null;
}
$expectedFieldNames = [];
foreach ((array)$expectedFields as $key => $expectedField) {
if (is_int($key)) {
$expectedFieldNames[] = $expectedField;
} else {
$expectedFieldNames[] = $key;
}
}
return sprintf($missingMessage, implode(', ', $expectedFieldNames));
}
}