Opposite of requireSecure in CakePHP - cakephp

CakePHP has a requireSecure function in the SecurityComponent. I'm using this to force SSL when passing sensitive information such as a credit card number.
Questions:
Is there a requireNonSecure function?
If there's no requireNonSecure function is it possible to extend/add a function to CakePHP's core file without modifying the original file?
I want a requireNonSecure function because some of my pages have embedded videos that can only be played on our domain name. When using SSL the video hosting service does not recognize our domain name and cannot play the videos.
This is some of the code in the beforeFilter of a controller:
function beforeFilter() {
parent::beforeFilter();
$this->Security->validatePost = false; // disable CSRF protection
$this->Security->blackHoleCallback = 'forceSSL';
$this->Security->requireSecure('pay', 'index');
$this->Auth->allow('index');
}
This is the callback in app_controller.php
function forceSSL() {
$redirect = '';
if (!empty($this->params['url']['redirect'])) {
$redirect = '?redirect=' . $this->params['url']['redirect'];
}
$this->redirect('https://' . rtrim(env('SERVER_NAME'), '/') . $this->here . $redirect);
}

A solution would be to append a function to beforeFilter like this:
In a controller:
function beforeFilter() {
parent::beforeFilter();
// Require non secure (http) for video action
$this->requireNonSecure('video');
// ... other code here
}
In app_controller.php:
function requireNonSecure() {
$requireNonSecure = array_map('strtolower', func_get_args());
if (in_array(strtolower($this->action), $requireNonSecure) || $requireNonSecure == array('*')) {
if ($this->RequestHandler->isSSL()) {
$this->redirect('http://' . rtrim(env('SERVER_NAME'), '/') . $this->here);
return;
}
}
}

This solution adds to SecurityComponent. It should work but there is a risk of redirect loops if both requireSecure and requireNonSecure are set.
SecurityPlusComponent:
class SecurityPlusComponent extends SecurityComponent {
/**
* List of actions that do not require an SSL-secured connection
*
* #var array
* #access public
* #see SecurityPlusComponent::requireNonSecure()
*/
var $requireSecure = array();
/**
* Component startup. All security checking happens here.
*
* #param object $controller Instantiating controller
* #access public
*/
function startup(&$controller) {
$this->_action = strtolower($controller->action);
$this->_methodsRequired($controller);
$this->_secureRequired($controller);
$this->_nonSecureRequired($controller);
$this->_authRequired($controller);
$this->_loginRequired($controller);
$isPost = ($this->RequestHandler->isPost() || $this->RequestHandler->isPut());
$isRequestAction = (
!isset($controller->params['requested']) ||
$controller->params['requested'] != 1
);
if ($isPost && $isRequestAction && $this->validatePost) {
if ($this->_validatePost($controller) === false) {
if (!$this->blackHole($controller, 'auth')) {
return null;
}
}
}
$this->_generateToken($controller);
}
function requireNonSecure() {
$this->_requireMethod('NonSecure', func_get_args());
}
/**
* Check if access requires non secure connection (http)
*
* #param object $controller Instantiating controller
* #return bool true if secure connection required
* #access protected
*/
function _nonSecureRequired(&$controller) {
if (is_array($this->requireNonSecure) && !empty($this->requireNonSecure)) {
$requireNonSecure = array_map('strtolower', $this->requireNonSecure);
if (in_array($this->_action, $requireNonSecure) || $this->requireNonSecure == array('*')) {
if ($this->RequestHandler->isSSL()) {
if (!$this->blackHole($controller, 'nonSecure')) {
return null;
}
}
}
}
return true;
}
}
Modified app_controller forceSSL function:
function securityBlackhole($type) {
$redirect = '';
if (!empty($this->params['url']['redirect'])) {
$redirect = '?redirect=' . $this->params['url']['redirect'];
}
// Force http (non-SSL)
if($type == 'nonSecure') {
$this->redirect('http://' . rtrim(env('SERVER_NAME'), '/') . $this->here . $redirect);
// Force https (SSL)
} else {
$this->redirect('https://' . rtrim(env('SERVER_NAME'), '/') . $this->here . $redirect);
}
}
Would be called like this in a controller:
function beforeFilter() {
parent::beforeFilter();
$this->SecurityPlus->validatePost = false; // disable CSRF protection
$this->SecurityPlus->blackHoleCallback = 'securityBlackhole';
$this->SecurityPlus->requireSecure('pay', 'index');
$this->SecurityPlus->requireNonSecure('video');
$this->Auth->allow('index');
}

Related

Can not config Header security middleware CakePHP

I have a site built with CakePHP, default it doesn't allow subdomain embed on iframe.
I configured Frame Option on nginx/conf.d and now my homepage can be embedded in an iframe of a subdomain.
However, another post cannot be embedded on a subdomain's iframe. (exp: http://example.net/postabc will not display). I tried to change options on Header security middleware(please read below).
Is there anywhere I need to change the configuration to so all my posts can be displayed?
<?php
namespace Cake\Http\Middleware;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class SecurityHeadersMiddleware
{
protected $headers = [];
public function noSniff()
{
$this->headers['x-content-type-options'] = 'nosniff';
return $this;
}
public function noOpen()
{
$this->headers['x-download-options'] = 'noopen';
return $this;
}
public function setReferrerPolicy($policy = 'same-origin')
{
$available = [
'no-referrer', 'no-referrer-when-downgrade', 'origin',
'origin-when-cross-origin',
'same-origin', 'strict-origin', 'strict-origin-when-cross-origin',
'unsafe-url'
];
$this->checkValues($policy, $available);
$this->headers['referrer-policy'] = $policy;
return $this;
}
public function setXFrameOptions($option = 'allow-from', $url = 'http://subdomain.example.net')
{
$this->checkValues($option, ['deny', 'sameorigin', 'allow-from']);
if ($option === 'allow-from') {
if (empty($url)) {
throw new InvalidArgumentException('The 2nd arg $url can not be empty when `allow-from` is used');
}
$option .= ' ' . $url;
}
$this->headers['x-frame-options'] = $option;
return $this;
}
public function setXssProtection($mode = 'block')
{
$mode = (string)$mode;
if ($mode === 'block') {
$mode = '1; mode=block';
}
$this->checkValues($mode, ['1', '0', '1; mode=block']);
$this->headers['x-xss-protection'] = $mode;
return $this;
}
public function setCrossDomainPolicy($policy = 'all')
{
$this->checkValues($policy, ['all', 'none', 'master-only', 'by-content-type', 'by-ftp-filename']);
$this->headers['x-permitted-cross-domain-policies'] = $policy;
return $this;
}
protected function checkValues($value, array $allowed)
{
if (!in_array($value, $allowed)) {
throw new InvalidArgumentException(sprintf(
'Invalid arg `%s`, use one of these: %s',
$value,
implode(', ', $allowed)
));
}
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
{
$response = $next($request, $response);
foreach ($this->headers as $header => $value) {
$response = $response->withHeader($header, $value);
}
return $response;
}
}

Bypass spool when using thirdparty database spooling

I'm using a thirdparty bundle (Dextervip Citrax\DatabaseSwiftmailerBundle) to spool my email in a database. However, I still want to be able to bypass the spooling for some specific actions. Before, when I didn't use the bundle and used just regular spooling, I did this the following way:
public function sendSeparateMessage($subject, $fromEmail, $toEmail, $toCc, $body, $bypassSpool = false){
$message = \Swift_Message::newInstance()
->setSubject($subject)
->setFrom($fromEmail)
->setTo($toEmail)
->setCc($toCc)
->setBody($body)
->setContentType("text/html");
// Send the message
$failedRecipients = [];
$numSent = 0;
foreach ($toEmail as $address => $name)
{
if (is_int($address)) {
$toEmail = $message->setTo($name);
} else {
$toEmail = $message->setTo([$address => $name]);
}
$numSent += $this->mailer->send($message, $failedRecipients);
}
if($bypassSpool) {
$spool = $this->mailer->getTransport()->getSpool();
$transport = \Swift_MailTransport::newInstance();
$spool->flushQueue($transport);
}
}
So whenever the last parameter of that function was set to true, the emails got sent out immediately and weren't stored for spooling.
For my situation now, I'd like to have the same thing, but of course I'm calling the wrong flushQueue function because I'm probably not accessing the right spooler. Does anybody know how to access the spooler from that bundle?
The service class from that bundle:
<?php
/**
* Created by PhpStorm.
* User: Rafael
* Date: 02/05/2015
* Time: 22:16
*/
namespace Citrax\Bundle\DatabaseSwiftMailerBundle\Spool;
use Citrax\Bundle\DatabaseSwiftMailerBundle\Entity\Email;
use Citrax\Bundle\DatabaseSwiftMailerBundle\Entity\EmailRepository;
use Swift_Mime_Message;
use Swift_Transport;
class DatabaseSpool extends \Swift_ConfigurableSpool {
/**
* #var EmailRepository
*/
private $repository;
private $parameters;
public function __construct(EmailRepository $repository, $parameters)
{
$this->repository = $repository;
$this->parameters = $parameters;
}
/**
* Starts this Spool mechanism.
*/
public function start()
{
// TODO: Implement start() method.
}
/**
* Stops this Spool mechanism.
*/
public function stop()
{
// TODO: Implement stop() method.
}
/**
* Tests if this Spool mechanism has started.
*
* #return bool
*/
public function isStarted()
{
return true;
}
/**
* Queues a message.
*
* #param Swift_Mime_Message $message The message to store
*
* #return bool Whether the operation has succeeded
*/
public function queueMessage(Swift_Mime_Message $message)
{
$email = new Email();
$email->setFromEmail(implode('; ', array_keys($message->getFrom())) );
if($message->getTo() !== null ){
$email->setToEmail(implode('; ', array_keys($message->getTo())) );
}
if($message->getCc() !== null ){
$email->setCcEmail(implode('; ', array_keys($message->getCc())) );
}
if($message->getBcc() !== null ){
$email->setBccEmail(implode('; ', array_keys($message->getBcc())) );
}
if($message->getReplyTo() !== null ){
$email->setReplyToEmail(implode('; ', array_keys($message->getReplyTo())) );
}
$email->setBody($message->getBody());
$email->setSubject($message->getSubject());
$email->setMessage($message);
$this->repository->addEmail($email);
}
/**
* Sends messages using the given transport instance.
*
* #param Swift_Transport $transport A transport instance
* #param string[] $failedRecipients An array of failures by-reference
*
* #return int The number of sent emails
*/
public function flushQueue(Swift_Transport $transport, &$failedRecipients = null)
{
if (!$transport->isStarted())
{
$transport->start();
}
$count = 0;
$emails = $this->repository->getEmailQueue($this->getMessageLimit());
foreach($emails as $email){
/*#var $message \Swift_Mime_Message */
$message = $email->getMessage();
try{
$count_= $transport->send($message, $failedRecipients);
if($count_ > 0){
$this->repository->markCompleteSending($email);
$count += $count_;
}else{
throw new \Swift_SwiftException('The email was not sent.');
}
}catch(\Swift_SwiftException $ex){
$this->repository->markFailedSending($email, $ex);
}
}
return $count;
}
}
and the services.yml file:
services:
repository.email:
class: Citrax\Bundle\DatabaseSwiftMailerBundle\Entity\EmailRepository
factory: ['#doctrine.orm.default_entity_manager',getRepository]
arguments: ['CitraxDatabaseSwiftMailerBundle:Email']
citrax.database.swift_mailer.spool:
class: Citrax\Bundle\DatabaseSwiftMailerBundle\Spool\DatabaseSpool
arguments: ['#repository.email', '%citrax_database_swift_mailer.params%']
swiftmailer.spool.db:
alias: citrax.database.swift_mailer.spool
swiftmailer.mailer.default.spool.db:
alias: citrax.database.swift_mailer.spool

Declaration of Upload::beforeSave() should be compatible with Model::beforeSave($options = Array) [APP/Model/Upload.php, line 5]

I am being shown the following error on top of my page when using beforeSave method in my Upload model.
Strict (2048): Declaration of Upload::beforeSave() should be
compatible with Model::beforeSave($options = Array)
[APP/Model/Upload.php, line 5]
Could someone point out what I'm doing wrong?
Here is my model:
<?php
App::uses('AppModel', 'Model');
class Upload extends AppModel {
protected function _processFile() {
$file = $this->data['Upload']['file'];
if ($file['error'] === UPLOAD_ERR_OK) {
$name = md5($file['name']);
$path = WWW_ROOT . 'files' . DS . $name;
if (is_uploaded_file($file['tmp_name'])
&& move_uploaded_file($file['tmp_name'], $path) ) {
$this->data['Upload']['name'] = $file['name'];
$this->data['Upload']['size'] = $file['size'];
$this->data['Upload']['mime'] = $file['type'];
$this->data['Upload']['path'] = '/files/' . $name;
unset($this->data['Upload']['file']);
return true;
}
}
return false;
}
public function beforeSave() {
if (!parent::beforeSave($options)) {
return false;
}
return $this->_processFile();
}
}
?>
Just change this line
public function beforeSave() {
to this, so you have correct method declaration
public function beforeSave($options = array()) {
The beforeSave() function executes immediately after model data has been successfully validated, but just before the data is saved. This function should also return true if you want the save operation to continue.
This callback is especially handy for any data-massaging logic that needs to happen before your data is stored. If your storage engine needs dates in a specific format, access it at $this->data and modify it.
Below is an example of how beforeSave can be used for date conversion. The code in the example is used for an application with a begindate formatted like YYYY-MM-DD in the database and is displayed like DD-MM-YYYY in the application. Of course this can be changed very easily. Use the code below in the appropriate model.
public function beforeSave($options = array()) {
if (!empty($this->data['Event']['begindate']) &&
!empty($this->data['Event']['enddate'])
) {
$this->data['Event']['begindate'] = $this->dateFormatBeforeSave(
$this->data['Event']['begindate']
);
$this->data['Event']['enddate'] = $this->dateFormatBeforeSave(
$this->data['Event']['enddate']
);
}
return true;
}
public function dateFormatBeforeSave($dateString) {
return date('Y-m-d', strtotime($dateString));
}
Make sure that beforeSave() returns true, or your save is going to fail.

Pass variable from controller to model (beforeSave) (Upload Field) CakePHP

I really need to know this issue to finish my work.
Here I'll give a example envolving model and controller.
I want to pass $final_name from controller to beforeSave() of my model
public function admin_add() {
if($this->request->is('post')) {
if($this->data['Client']['file']['tmp_name'] != '') {
// Upload block
$tmp_file = $this->data['Client']['file']['tmp_name'];
$file = new File($tmp_file);
if($file->mime() == "image/jpeg" or "image/png") {
$ext = explode('.', $this->data['Client']['file']['name']);
$name = md5($this->data['Client']['file']['name']);
$file->copy(IMG_DIR . 'portfolio\\' . $name . '.' . end($ext));
$final_name = $name . "." . end($ext); // File name with extension
}
// If save
if($this->Client->save($this->request->data)) {
$this->Session->setFlash('Client cadastrado com sucesso!', 'admin_flash');
}
}
}
}
In my client model
public function beforeSave($options = array()) {
if($this->data['Client']['file']['name'] != null) {
$this->data['Client']['file'] = $final_name;
}
return parent::beforeSave($options);
}
In Controller
$this->request->data['Client']['final_name'] = $name . "." . end($ext);
In Model
public function beforeSave($options = array()) {
if($this->data['Client']['file']['name'] != null) {
$this->data['Client']['file'] = $this->data['Client']['final_name'];
}
return parent::beforeSave($options);
}
Update for CakePHP3
Pass variable from controller to table beforeSave, afterSave ?
// Examples
// In Controller
$this->Article->save($data, ['passVariable' => 'passedData']);
// in Table
public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
{
if (isset($options['passVariable'])) {
// implement your code
}
}
Read more: https://api.cakephp.org/3.8/class-Cake.ORM.Table.html#_save
But good place to modify data before save like asked in question are:
https://book.cakephp.org/3.0/en/orm/saving-data.html#before-marshal or
https://book.cakephp.org/3.0/en/orm/entities.html#accessors-mutators

CakePHP REST Authorization issue on POST and PUT (I get 404)

I'm developing a REST api for a application, and everething went fine up until now...
I'm building a header with login data, GET and DELETE work fine but when I try to send a PUT or POST request I get 404...
When authorization is off (i.e., I do not check it in cake) everything works fine.
Here's the controller code:
class SitesController extends AppController {
var $uses = array("Site");
var $name = 'Sites';
var $scaffold;
var $components = array('RequestHandler','Security');
function beforeFilter() {
$this->Security->loginOptions = array(
'type'=>'basic'
);
$this->Security->loginUsers = array(
'lukasz'=>'blabla',
'test'=>'test'
);
$this->Security->requireLogin();
}
function index() {
$sites = $this->Site->find('all');
$this->set(compact('sites'));
}
function view($id) {
$site = $this->Site->findById($id);
$this->set(compact('site'));
}
function add() {
if($this->data != null) {
$this->Site->create();
if($this->Site->save($this->data)) {
$message = array('Deleted');
} else {
$message = $this->data;
}
$this->set(compact("message"));
}
}
function edit($id) {
$this->Site->id = $id;
if ($this->Site->save($this->data)) {
$message = array('Saved');
} else {
$message = array('Error');
}
$this->set(compact("message"));
}
function delete($id) {
if($this->Site->delete($id)) {
$message = array('Deleted');
} else {
$message = array('Error');
}
$this->set(compact("message"));
}
}
And here's how I send requests:
http://bin.cakephp.org/view/165115685
http://bin.cakephp.org/view/1477117088
I suspect you're running into the CSRF protection (form spoofing protection) the SecurityComponent applies to all POST and PUT requests. Try turning it off using the $validatePost option.

Resources