Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Best CSRF setup

144 views
Skip to first unread message

Jeffrey Röling

unread,
May 3, 2024, 11:33:26 AM5/3/24
to Fat-Free Framework
I struggling with the CSRF part. Want it to be in the session but must work when user is logged in or out. what is the best way to setup. Place it into the before route for everything, of yes check it before executing a post command?

v.

unread,
May 3, 2024, 5:09:21 PM5/3/24
to Fat-Free Framework

Jeffrey Röling

unread,
May 4, 2024, 4:04:41 PM5/4/24
to Fat-Free Framework
When showing all CSRF tokens on the webpage; CSRF/SESSION.CSRF/POST.CSRF the are the same. When filling in the form all this CSRF tokens are different when putting a logger in the log files.
This happened when I was updating from php 7 to 8 not sure it some thing is change in php that could cause this.

v.

unread,
May 6, 2024, 3:24:11 AM5/6/24
to Fat-Free Framework
without seeing your code there is really nothing anyone can say

Jeffrey Röling

unread,
May 6, 2024, 11:28:07 AM5/6/24
to Fat-Free Framework
I found out that it's only with 1 page the case that CSRF doesn't work ask expected.
Below the codes. And seems that in the page for the reservation "reservation.html" is something that triggering the CSRF attack.

Index.php
<?php
// Hocus Pocus, grab the focus
// Magic. Do not touch.

// Kickstart the framework
require 'vendor/autoload.php';

// Load F3
$f3 = \Base::instance();

// Set some configuration stuff
$f3->set('AUTOLOAD',array('app/',function($class){
return strtoupper($class);
}));
$f3->set('UI','app/view/');
$f3->set('ESCAPE', false);
$f3->set('LOGS','logs/');
$f3->set('UPLOADS','uploads/');
$f3->set('ASSETS',$f3->get('BASE').'/assets/');
$f3->set('CACHE','true');
$f3->set('TZ',$f3->get('settings.timezone'));
$f3->set('PREFIX','DICT.');
$f3->set('LOCALES','app/dict/');
$f3->set('FALLBACK','nl');
$f3->set('ENCODING','UTF-8');

// Session Handle
$session_db = new \DB\SQL("mysql:host=" . $f3->get("database.host") . ";port=" . $f3->get("database.port") . ";dbname=" . $f3->get("database.name"),$f3->get("database.user"),$f3->get("database.pass"), array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION));
$f3->session = new \DB\SQL\Session($session_db,'sessions',TRUE,NULL,'CSRF');

// Execute the Applications
$f3->run(); // Let's go!!
$f3->set('SESSION.previousUrl',$f3->REALM);

Controller.php
class Controller {
protected $f3;
protected $db;
protected $template;
protected $flash;
protected $validator;
protected $today;

function beforeroute() {
// Inactivity Check
if( $this->f3->get('SESSION.granted') ) {
if( time() - $this->f3->get('SESSION.timestamp') > $this->f3->get('auto_logout') ) {
$this->f3->clear('SESSION');
$this->f3->reroute('@home');
} else {
$this->f3->set('SESSION.timestamp', time());
}
}
// CSRF Prevention
if( NULL === $this->f3->get('POST.token') ){
$this->f3->CSRF = $this->f3->session->csrf();
$this->f3->copy('CSRF','SESSION.csrf');
}
if( $this->f3->VERB === 'POST' ){
if( $this->f3->get('POST.token') == $this->f3->get('SESSION.csrf') ) {
// Thing check out! No CSRF attack was detectd.
$this->f3->logger->write( "SUCCESS - CSRF: ".$this->f3->get('CSRF')." | SESSION: ".$this->f3->get('SESSION.csrf')." | POST ".$this->f3->get('POST.token'),'r' );
$this->f3->set('CSRF', $this->f3->session->csrf()); // Reset CSRF token for next post request.
$this->f3->copy('CSRF','SESSION.csrf'); // Copy the token to the variable.
} else {
// DANGER: CSRF Attack!
$this->f3->logger->write( "FAILED - CSRF: ".$this->f3->get('CSRF')." | SESSION: ".$this->f3->get('SESSION.csrf')." | POST ".$this->f3->get('POST.token'),'r' );
$this->flash->addMessage($this->f3->get('DICT.csrf'), 'danger');
$this->f3->reroute('@home');
}
}


Reservation.php
// Loads the index for items (loads all items)
public function reservation() {
$this->f3->set('title', $this->f3->get('DICT.nav.reservation'));
$this->f3->set('view', 'app/pages/reservations/reservation.htm');

// Collects an list of customers in the system
$customers = new \Models\Customers();
$this->f3->set('customers', $customers->find(['active = 1'], ['order' => 'name ASC']));

// Collects an list of sections in the system
$sections = new \Models\Materials\Sections();
$this->f3->set('sections', $sections->find(null, ['order' => 'id ASC']));

// Collects an list of categories in the system
$categories = new \Models\Materials\Categories();
$this->f3->set('categories', $categories->find(['active = 1'], ['order' => 'form_order ASC']));

// Collects an list of seasons in the system
$seasons = new \Models\Reservations\Seasons();
$this->f3->set('seasons', $seasons->find(null, ['order' => 'name ASC']));

echo $this->template->render('templates/default/layout.htm');
}

// Create a new Reservation
public function create() {
$reservation = new \Models\Reservations\Reservation();

if($this->f3->exists('POST.send')){
$this->_create();
$this->f3->logger->write( "[RESERVATION] CSRF: ".$this->f3->get('CSRF')." | SESSION: ".$this->f3->get('SESSION.csrf')." | POST ".$this->f3->get('POST.token'),'r' );
}
$this->f3->reroute('@home');
}

function _create() {
$reservation = new \Models\Reservations\Reservation();
$reservation->copyFrom('POST','customer;email;pickup_date;return_date;season;status;notes;categories');
$reservation->uid = uniqid('r');
$reservation->request_date = $this->today;
$reservation->save();
$this->flash->addMessage($this->f3->get('DICT.resevations.msg.createSuccess'), 'success');

$data = array(
"name" => $reservation->customer->name,
"email" => $reservation->email,
"title" => "Reservering",
"uid" => $reservation->uid,
);
$mail = new \Helpers\Mail();
$mail->mail($data['title'], 'reservation/customer', $data, false);
}

Reservation.html
<div class="row pb-2">
<form role="form" method="POST" enctype="multipart/form-data" action="{{ @BASE }}{{ 'reservationCreate' | alias }}">
<div class="col-sm-12 col-md-12 col-lg-8 m-0 p-0 d-grid gap-2">
<!-- General -->
<div class="card p-0 m-0">
<h5 class="card-header p-2">{{ ucfirst(@DICT.reservations.details) | format }}</h5>
<div class="card-body p-2">
<div class="form-group p-1 m-0">
<select class="form-select" name="customer" required>
<option selected disabled hidden>{{ ucfirst(@DICT.reservations.customer) | format }}</option>
<repeat group="{{ @customers }}" value="{{ @customer }}">
<check if="{{ @customer.active }}">
<true><option value="{{ @customer.id }}">{{ ucfirst(@customer.name) | format }}</option></true>
</check>
</repeat>
</select>
</div>
<div class="form-group p-1 m-0">
<select class="form-select" name="season" required>
<option selected disabled hidden>{{ ucfirst(@DICT.reservations.season) | format }}</option>
<repeat group="{{ @seasons }}" value="{{ @season }}">
<true><option value="{{ @season.id }}">{{ ucfirst(@season.name) | format }}</option></true>
</repeat>
</select>
</div>
<div class="input-group p-1 m-0">
<span class="input-group-text col-3">{{ ucfirst(@DICT.reservations.pickup) | format }}</span>
<input type="date" name="pickup_date" id="datepicker_1" class="form-control col-3" required />
<span class="input-group-text col-3">{{ ucfirst(@DICT.reservations.return) | format }}</span>
<input type="date" name="return_date" id="datepicker_2" class="form-control col-3" required />
</div>
<div class="form-group p-1 m-0">
<input type="email" class="form-control" name="email" placeholder="{{ ucfirst(@DICT.reservations.email) | format }}" />
</div>
<div class="form-group p-1 m-0">
<textarea class="form-control" name="notes" placeholder="{{ ucfirst(@DICT.notes) | format }}"></textarea>
</div>
</div>
</div>

<!-- Generates the sections with items -->
<repeat group="{{ @sections }}" value="{{ @section }}">
<include href="app/pages/reservations/helpers/section.htm" with="categories={{ @categories }},title={{ @section.name }},sectionId={{ @section.id }}"/>
</repeat>
<!-- Submit -->
<input type="hidden" name="token" value="{{ @CSRF }}" />
<input type="hidden" name="send" value="send" />
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<input type="submit" value="{{ ucfirst(@DICT.buttons.send) | format }}" class="btn btn-primary" />
</div>
</div>
</form>
</div>
<script type="text/javascript">

// Block to select historical dates, only today and in the future.
$(function(){
var dtToday = new Date();
var month = (dtToday.getMonth() + 1).toString().padStart(2, '0');
var day = dtToday.getDate().toString().padStart(2, '0');
var maxDate = dtToday.toISOString().substr(0, 10);
$('#datepicker_1').attr('min', maxDate);
$('#datepicker_2').attr('min', maxDate);
});
</script>




Section.htm
{* INCLUDE <include href="app/pages/reservations/helpers/section.htm" with="categories=,title=,sectionId="/> *}

{* SECTION *}
<div class="card p-0 m-0">
<h5 class="card-header p-2">{{ ucfirst(@section.name) | format }}</h5>
<div class="card-body p-2">
<repeat group="{{ @categories }}" value="{{ @category }}">
<check if="{{ @category.section.id == @sectionId }}">
<true>
<div class="input-group p-1 m-0">
<span class="input-group-text col-8">{{ ucfirst(@category.category) | format }}</span>
<input class="form-control" type="number" name="categories[{{ @category.id }}]" min="{{ @category.form_min }}" max="{{ @category.form_max }}" step="1" placeholder="0">
<span class="input-group-text">{{ @category.form_min }}/{{ @category.form_max }}</span>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#photo_{{ @category.id }}"><i class="fas fa-camera"></i></button>
</div>
<div class="modal fade" id="photo_{{ @category.id }}" data-bs-backdrop="static" role="dialog">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-body p-1 m-0">
<check if="{{ @category.image }}">
<true>
<img src="{{ @BASE.'/'.@category.image }}" width="100%" height="100%" alt="{{ @category.category }}">
</true>
<false>
<img src="{{ @BASE.'/uploads/noimage.jpg'}}" width="100%" height="100%" alt="{{ @category.category }}">
</false>
</check>
<div class="modal-footer p-1 m-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ucfirst(@DICT.buttons.close) | format }}</button>
</div>
</div>
</div>
</div>
</div>
<div class="form-text ps-1 m-0">{{ ucfirst(@category.form_notes) | format }}</div>
</true>
</check>
</repeat>
</div>
</div>

Jeffrey

unread,
May 6, 2024, 5:32:44 PM5/6/24
to Fat-Free Framework
I have tried to change, but that didn't work.
$f3->session = new \DB\SQL\Session($session_db,'sessions',TRUE,NULL,'CSRF'); to $f3->session = new \DB\SQL\Session($session_db);

Jeffrey

unread,
May 15, 2024, 9:44:51 AM5/15/24
to Fat-Free Framework
any suggestions?

Jeffrey

unread,
May 15, 2024, 4:20:01 PM5/15/24
to Fat-Free Framework
I narrowed it down to this location the commented area with {* ISSUE *** *}

<check if="{{ @category.image }}">
<true>
{* ISSUE <img src="{{ @BASE.'/'.@category.image }}" width="100%" height="100%" alt="{{ @category.category }}">*}
</true>
<false>
{* ISSUE <img src="{{ @BASE.'/uploads/noimage.jpg'}}" width="100%" height="100%" alt="{{ @category.category }}">*}
</false>
</check>

So seems to go wrong with {{ @BASE.'/'.@category.image }} end {{ @BASE.'/uploads/noimage.jpg'}} that results in a sort of reroute again.

v.

unread,
May 15, 2024, 5:07:24 PM5/15/24
to Fat-Free Framework
Not sure what is happening. Maybe a route-setting?
Just had a look at my rather old code and not even sure anymore what I did there...

Try setting a session.csrf at the very beginning of the controller:
if(null==$this->f3->get("SESSION.csrf"){
     $this->f3->set("SESSION.csrf", $this->f3->session->csrf() )
}
And don't reset it after every (post or other) request. Sure it is more secure in theory, but I never heard of any real world problems that were solved with it and it is a headache when users are opening pages in different browser tabs.

ved

unread,
May 16, 2024, 3:18:10 PM5/16/24
to Fat-Free Framework
Hey,

Image src requests shouldn't be affecting your app.

It appears as image requests are also being routed through PHP (assuming the line you identified as being the cause is correct)

What is your webserver/webserver config?

Cheers

Jeffrey

unread,
May 16, 2024, 5:11:07 PM5/16/24
to Fat-Free Framework
Hello,

Do you main this part of the webserver config?
It's running httpd on fedora 40 with php version 8.3.6
In the map /var/www/html is a folder called Development and in the materiaalbeheer so path is "/var/www/html/Development/materiaalbeheer" so url is "http://10.10.10.10/Development/materiaalbeheer/nl/"

<Directory "/var/www/html">

    Options Indexes FollowSymLinks

    AllowOverride All

    Require all granted

</Directory>


<Directory "/var/www">

    AllowOverride None

    Require all granted

</Directory>


<Directory />

    AllowOverride none

    Require all denied

</Directory>

ved

unread,
May 16, 2024, 5:34:08 PM5/16/24
to Fat-Free Framework
Hi,

Well, yes but also your .htaccess file in that case as it appears you're using apache.

But if you're using a similar one to the one on the documentation, it should be ok and not be the problem after all.

ikkez

unread,
May 16, 2024, 6:53:40 PM5/16/24
to Fat-Free Framework
well, in case an image file is not existing, it'll lead to a 404.. so the 404 error is actually routed through F3.. and depending on the error handler settings, custom erroer handle etc... there's a possibility that you actually changed the CSRF token when a 404 page is rendered.

Rishabh Raj

unread,
May 17, 2024, 3:12:33 AM5/17/24
to Fat-Free Framework
Can You help me.  I am using ajax PHP, and try to regenerate csrf token each time when I send a request but it doesn't work. Can you explain  by giving example also provide github link in which you have explained

Jeffrey

unread,
May 17, 2024, 3:40:43 AM5/17/24
to Fat-Free Framework
Currently my .htaccess looks like this.
# Enable rewrite engine and route requests to framework
RewriteEngine On

# Some servers require you to specify the `RewriteBase` directive
# In such cases, it should be the path (relative to the document root)
# containing this .htaccess file
#
RewriteBase /Development/materiaal/

RewriteRule ^(app|tmp)\/|\.ini$ - [R=404]

RewriteCond %{REQUEST_FILENAME} !-l
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [L,QSA]
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]



ved

unread,
May 17, 2024, 7:29:21 AM5/17/24
to Fat-Free Framework
Hi,

.htaccess looks ok.

But as ikkez mentioned, when an image doesn't exist, the request will be routed through F3 to show the 404 error and that may be where your CSRF is changing.

On your <check> condition, are you sure @category.image is a valid image path?

Can you show us the webserver logs while you're accessing the affected urls? That should show if you're getting 404 errors for your image requests. (browser developer tools network tab should also work)

Cheers.

Jeffrey

unread,
May 21, 2024, 6:50:43 AM5/21/24
to Fat-Free Framework
The @category.image is empty when no values is in the database then it must be noimage.jpg
Reply all
Reply to author
Forward
0 new messages