How to parse / validate / handle http headers in PHP - validation

Currently i am building my own php framework and i am now creating an implementation of the PHP-FIG PSR-7 MessageInterface. In specific the withHeader method. It states that the method could trow an exeption: \InvalidArgumentException for invalid header names or values.
So i am wondering, when is a header valid or invalid? Same for the values.
Or should i accept any header, and any header value? That could be dangerous right?
I now you can generaly say that if a header has multiple values, they are comma seperated. But that does not allways apply. If i look at a user-agent header for example, the value itself sometimes contains a comma. But you should treat it as a single value.

Indeed, it's "dangerous" to pass a header name - as argument of withHeader(), which
is NULL
is not a string
is an empty string
The same applies to the header value argument. It must be an array or a string (representing only one value, not a comma-separated list of values!).
As for the implementation of the withHeader method:
/**
* Return an instance with the provided value replacing the specified header.
*
* ...
*
* #param string $name Case-insensitive header field name.
* #param string|string[] $value Header value(s).
* #return static
* #throws \InvalidArgumentException for invalid header names or values.
*/
public function withHeader($name, $value) {
$this
->validateHeaderName($name)
->validateHeaderValue($value)
;
$clone = clone $this;
$clone->replaceHeader($name, $value);
return $clone;
}
/**
* =================
* Not part of PSR-7
* =================
*
* Validate header name.
*
* #param string $name Case-insensitive header field name.
* #return $this
* #throws \InvalidArgumentException
*/
protected function validateHeaderName($name) {
if (!isset($name)) {
throw new \InvalidArgumentException('No header name provided!');
}
if (!is_string($name)) {
throw new \InvalidArgumentException('The header name must be a string!');
}
if (empty($name)) {
throw new \InvalidArgumentException('Empty header name provided!');
}
return $this;
}
/**
* =================
* Not part of PSR-7
* =================
*
* Validate header value.
*
* #param string|string[] $value Header value(s).
* #return $this
* #throws \InvalidArgumentException
*/
protected function validateHeaderValue($value) {
if (isset($value) && !is_array($value) && !is_string($value)) {
throw new \InvalidArgumentException('The header value must be a string or an array!');
}
return $this;
}
/**
* =================
* Not part of PSR-7
* =================
*
* Replace a header item with a new one.
*
* #param string $name Case-insensitive header field name.
* #param string|string[] $value Header value(s).
* #return $this
* #done
*/
protected function replaceHeader($name, $value) {
$this
->removeHeader($name)
->addHeader($name, $value)
;
return $this;
}

You can find that in RFC 7230. Check Zend Diactoro's HeaderSecurity class for an implementation.

Related

Optimize a method that checks url segments issue

I've been using a method in Laravel Middleware that checks for strings in any URL segment to block the IP if it matches the "blacklisted" strings.
In the beginning, I had just a few strings to check, but now, the list is growing, and when I tried to optimize it to use a blacklist array, I ended up in a complete mess in the code and in my mind.
I believe this can be done but can't figure out the best way to optimize this middleware. Below is a sample of the Middleware code with notes where I'm having trouble.
In the handle($request, Closure $next) method is calling the $this->inUrl() method for all the blacklisted strings.
I've tried to add a protected $blacklisted array, to be used in the $this->inUrl() but can't make it work.
Thank you in advance for any suggestions that would be much appreciated and welcome. I am also thinking of providing the code as a gist on GitHub when optimized.
namespace App\Http\Middleware;
/**
* Class VerifyBlacklistedRequests
*
* #package App\Http\Middleware
*/
class VerifyBlacklistedRequests
{
/**
* The array of blacklisted request string segments
*
* #access protected
* #var array|string[]
*/
protected array $blacklisted = [
'.env', '.ftpconfig', '.vscode', ',git', '.git/HEAD'
// etc...
];
/**
* Handle an incoming request.
*
* #access public
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
*/
public function handle($request, Closure $next)
{
if($this->inUrl('.env')
|| $this->inUrl('.ftpconfig')
|| $this->inUrl('.vscode')
|| $this->inUrl('.git')
|| $this->inUrl('.git/HEAD')
// many more checks below the above ones
) {
// logic that blocks the IP goes here and working fine
}
return $next($request);
}
/**
* Check if the string is in any URL segment or at the one specified.
*
* #access protected
*
* #param string|mixed $value Segment value/content.
* #param integer $segment Segment position.
*
* #return bool
*/
protected function inUrl(string $value, $segment = -1)
{
if($segment !== -1 && request()->segment($segment) === $value) {
return true;
}
collect(request()->segments())->each(function ($segment) use ($value) {
if($segment === $value) {
return true;
}
});
return false;
}
}
After all the suggestions, kindly posted here, I ended up with a solution that uses some of the suggested methods.
The result ended up by reducing the pages' loading time by more than 1 second.
My final implementation:
Created a config file security.php which contains the blacklisted request strings, and a shortlist of whitelisted IPs.
The security.php config file
<?php
return [
/*
|--------------------------------------------------------------------------
| Whitelisted IPs configuration
|--------------------------------------------------------------------------
|
| These are the settings for the whitelisted IPs. The array contains
| the IPs that should not trigger the IP block.
|
*/
'whitelisted_ips' => [
// whitelisted IPs array
],
/*
|--------------------------------------------------------------------------
| Blacklisted request strings configuration
|--------------------------------------------------------------------------
|
| These are the settings for the blacklisted request strings. The array contains
| the strings that should trigger the IP to be blocked.
|
*/
'blacklisted_requests' => [
'.env',
'.ftpconfig',
'.vscode',
'.git',
'.git/HEAD',
'_profiler',
'__media__',
'administrator',
//...
];
];
Optimized the middleware removing the loops on the inUrl() method
The VerifyBlacklistedRequests middleware
<?php
namespace App\Http\Middleware;
use Closure;
/**
* Class VerifyHackingAttemptsRequests
*
* #property \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed white_listed_ips
* #property \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed blacklist
* #package App\Http\Middleware
*/
class VerifyHackingAttemptsRequests
{
/**
* #access protected
* #var \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed
*/
protected $blacklist;
/**
* #access protected
* #var \Illuminate\Config\Repository|\Illuminate\Contracts\Foundation\Application|mixed
*/
protected $white_listed_ips;
/**
* VerifyHackingAttemptsRequests constructor
*
* #access public
*/
public function __construct()
{
$this->blacklist = config('security.blacklisted_requests');
$this->white_listed_ips = config('security.whitelisted_ips');
}
/**
* Handle an incoming request.
*
* #access public
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
* #since 2.8.1
*/
public function handle($request, Closure $next)
{
$exists = false;
foreach(request()->segments() as $segment) {
if(in_array($segment, $this->blacklist)) {
$exists = true;
}
}
if($exists) {
$this->blockIp($request)
}
return $next($request);
}
/**
* Method to save an IP in the Blocked IP database table
*
* #access protected
*
* #param \Illuminate\Http\Request $request
*
* #return \App\Models\BlockedIp
*/
protected function blockIp(Request $request, $notes = null)
{
// the logic to persist the data through the BlockedIp model
}
}
In summary, the inUrl() method was removed, removing all the loops and method calls and, as mentioned above, the pages' loading time was sliced by more than 50%.
Thanks to all for the suggested methods which contributed to helping me solve the problem.
I recommend you to create literal routes, so it is easier to maintain. Go to RouteServiceProvider and create a new reading similar to web or api, so any route that is in that new file, it will ban/block the IP.
I don't know if doing this will optimize the code but the code is much more readable I think.
namespace App\Http\Middleware;
/**
* Class VerifyBlacklistedRequests
*
* #package App\Http\Middleware
*/
class VerifyBlacklistedRequests
{
/**
* The array of blacklisted request string segments
*
* #access protected
* #var array|string[]
*/
protected array $blacklisted = [
'.env', '.ftpconfig', '.vscode', ',git', '.git/HEAD'
// etc...
];
/**
* Handle an incoming request.
*
* #access public
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
*
* #return mixed
*/
public function handle($request, Closure $next)
{
//loop over the list instead of that long conditions
foreach($this->blacklisted as $blacklistedItem) {
if($this->inUrl($blacklistedItem))
{
// logic that blocks the IP goes here and working fine
}
}
}
/**
* Check if the string is in any URL segment or at the one specified.
*
* #access protected
*
* #param string|mixed $value Segment value/content.
* #param integer $segment Segment position.
*
* #return bool
*/
protected function inUrl(string $value, $segment = -1)
{
if($segment !== -1 && request()->segment($segment) === $value) {
return true;
}
foreach(request()->segments() as $segment) {
if($segment === $value) {
return true;
}
}
return false;
}
}
I believe you can use DB and make the blacklist as indexed in DB,
Database would handle and search for this itself with its inner Engine.
protected function urlWasInBlackList($segment = -1){
$segmentStrings= $segment != -1 ? [request()->segment($segment)] : request()->segments();
return DB::table('blacklisted')->select('*')->whereIn('pattern',$segmantStrings)->exists()
}
and using some function like this instead of the inUrl(). it will check against the forbidden words Against the DB.
Update : Avoid DB
however DB handles this situation with its InnoDB engine quickly,
maybe you want to avoid DB, because of connection overhead or simply not dependent your project just because one functionality,
if you want to a avoid DB,
you have to check url with each black listed word with a loop and it would be O(n),
and if you multiply sections count, it would be O(n*m). The
Optimal way is to avoid loop, and make a hash table like what DB does.
so I go for hash_tables in php and found in the php array doc
that php associative arrays are hash map.
and the function array_key_exists() is looking into hash table for keys, you can see in the php source for the array_key_exists() codes which addressing the ht as hash map here:
array.c line #6071
zen_hash.h line #529
so I suggest to use something like this:
protected array $blacklisted = [
'.env' => null, '.ftpconfig' => null, '.vscode' => null, ',git' => null, '.git/HEAD' => null
];
for blacklist definitions.
and
$segmentStrings= $segment != -1 ? [request()->segment($segment)] : request()->segments();
foreach(segmentStrings as $segmentString){
return array_key_exists($segmentString, $blacklisted);
}

Codeigniter 4 filters don't works

hi i want create my own authorization to study the new veri=sion of framework ...
this is my route :
$routes->add('/user/login', 'User::login',['filter'=>'usersFiltersNoAuth']);
$routes->add('/login', 'User::login',['filter'=>'usersFiltersNoAuth']);
$routes->add('/user/registration', 'User::registration',['filter'=>'usersFiltersNoAuth']);
$routes->add('/logout', 'User::logout');
$routes->add('/user/changeEmail', 'User::changeEmail',['filter'=>'usersFiltersAuth']);
$routes->add('/user/changePassword', 'User::changePassword',['filter'=>'usersFiltersAuth']);
And this is my 2 filter class:
class UsersFiltersNoAuth implements FilterInterface
{
/**
* Do whatever processing this filter needs to do.
* By default it should not return anything during
* normal execution. However, when an abnormal state
* is found, it should return an instance of
* CodeIgniter\HTTP\Response. If it does, script
* execution will end and that Response will be
* sent back to the client, allowing for error pages,
* redirects, etc.
*
* #param \CodeIgniter\HTTP\RequestInterface $request
* #param array|null $params
*
* #return mixed
*/
public function before(RequestInterface $request, $params = null)
{
// if no user is logged in then send them to the login form
if (isset($_SESSION['user_id']))
{
return redirect()->to('/user/index');
}
}
//--------------------------------------------------------------------
/**
* Allows After filters to inspect and modify the response
* object as needed. This method does not allow any way
* to stop execution of other after filters, short of
* throwing an Exception or Error.
*
* #param \CodeIgniter\HTTP\RequestInterface $request
* #param \CodeIgniter\HTTP\ResponseInterface $response
* #param array|null $arguments
*
* #return void
*/
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
}
//--------------------------------------------------------------------
} // End of UsersFiltersNoAuth Class.
class UsersFiltersAuth implements FilterInterface
{
/**
* Do whatever processing this filter needs to do.
* By default it should not return anything during
* normal execution. However, when an abnormal state
* is found, it should return an instance of
* CodeIgniter\HTTP\Response. If it does, script
* execution will end and that Response will be
* sent back to the client, allowing for error pages,
* redirects, etc.
*
* #param \CodeIgniter\HTTP\RequestInterface $request
* #param array|null $params
*
* #return mixed
*/
public function before(RequestInterface $request, $params = null)
{
// if no user is logged in then send them to the login form
if (!isset($_SESSION['user_id']))
{
session()->set('redirect_url', current_url());
return redirect()->to('/login');
}
}
//--------------------------------------------------------------------
/**
* Allows After filters to inspect and modify the response
* object as needed. This method does not allow any way
* to stop execution of other after filters, short of
* throwing an Exception or Error.
*
* #param \CodeIgniter\HTTP\RequestInterface $request
* #param \CodeIgniter\HTTP\ResponseInterface $response
* #param array|null $arguments
*
* #return void
*/
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
}
//--------------------------------------------------------------------
} // End of UsersFiltersAuth Class.
if i try to go to /user/chengeEmail or /user/changePassword when ($_SESSION['user_id] is set) i am redirect to /user/index why ?
Moreover there is a way to apply a filter to an entire controller ? except some method ?

how to save and retrieve base64 encoded data using Laravel model

I am doing a web app using Laravel 7 api.
I receive data with a json request that I must store in the db with a base64 encoding.
I store the data in this way:
public function create_request(Request $request)
{
$req_store = new Req;
$req_store->text = base64_encode($request->input('requestInformations.text'));
$req_store->save();
}
Then obviously to retrieve the data of the text column I must use everytime base64_decode().
My question is that: is there a way to say to Laravel that everytime that I store a new request the column text must be authomatically encoded to base64 and everytime that I retrieve that data from the database the field text must be authomatically base64 decoded? I suppose I must write something in the Req.php model...
Can help?
You should use Accessors & Mutators for this. Please follow the link https://laravel.com/docs/7.x/eloquent-mutators#accessors-and-mutators
public function getTextAttribute($value)
{
return base64_decode($value);
}
public function setTextAttribute($value)
{
$this->attributes['text'] = base64_encode($value);
}
You may use a custom caster
https://laravel.com/docs/7.x/eloquent-mutators#custom-casts
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class Base64 implements CastsAttributes
{
/**
* Cast the given value.
*
* #param \Illuminate\Database\Eloquent\Model $model
* #param string $key
* #param mixed $value
* #param array $attributes
* #return mixed
*/
public function get($model, $key, $value, $attributes)
{
return base64_decode($value);
}
/**
* Prepare the given value for storage.
*
* #param \Illuminate\Database\Eloquent\Model $model
* #param string $key
* #param array $value
* #param array $attributes
* #return string
*/
public function set($model, $key, $value, $attributes)
{
return base64_encode($value);
}
}
And in your model
use App\Casts\Base64;
/**
* The attributes that should be cast.
*
* #var array
*/
protected $casts = [
'text' => Base64::class,
];

while running http://127.0.0.1:8000/products, it says products.index not found

File Directory: C:\laragon\www\first-laravel-app\vendor\laravel\framework\src\Illuminate\View\FileViewFinder.php
/**
* Find the given view in the list of paths.
*
* #param string $name
* #param array $paths
* #return string
*
* #throws \InvalidArgumentException
*/
protected function findInPaths($name, $paths)
{
foreach ((array) $paths as $path) {
foreach ($this->getPossibleViewFiles($name) as $file) {
if ($this->files->exists($viewPath = $path.'/'.$file)) {
return $viewPath;
}
}
}
throw new InvalidArgumentException("View [{$name}] not found.");
}
/**
* Get an array of possible view files.
*
* #param string $name
* #return array
*/
protected function getPossibleViewFiles($name)
{
return array_map(function ($extension) use ($name) {
return str_replace('.', '/', $name).'.'.$extension;
}, $this->extensions);
}
/**
* Add a location to the finder.
*
* #param string $location
* #return void
Arguments
"View [products.index] not found."
When attempting to access the page at http://127.0.0.1:8000/products, it says products.index not found.
Because you are initializing a route as a resource you need to define methods for each of the possible routes in your ProductController class.
See: Laravel - Route::resource vs Route::controller

Custom generator command, is not stopping when create file that already exists

So, i'm trying to use PHP Artisan on Laravel 5.3 to create a class file for each Cron configuration in my project, i'm doing this because it's possible that i'll want to create these files from a separate GUI in the future.
I'm able to create the files, and i'm using stubs so everything gets generated as it should, the problem however is that for some reason, if a file, say "cron_4" exists and i call my custom command php artisan make:cron cron_4 it'll allow me to do so and will simply overwrite the existing file.
This is my code so far. Any ideas as to what i might be doing wrong here?
<?php
namespace App\Console\Commands;
use Illuminate\Console\GeneratorCommand;
use Symfony\Component\Console\Input\InputOption;
class CronMakeCommand extends GeneratorCommand
{
/**
* The console command name.
*
* #var string
*/
protected $name = 'make:cron';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Create a new Cron class';
/**
* The type of class being generated.
*
* #var string
*/
protected $type = 'Cron';
/**
* Get the stub file for the generator.
*
* #return string
*/
protected function getStub()
{
return __DIR__.'/stubs/cron.stub';
}
/**
* Get the default namespace for the class.
*
* #param string $rootNamespace
* #return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace.'\Crons';
}
/**
* Execute the console command.
*
* #return void
*/
public function fire()
{
if (! $this->option('id')) {
return $this->error('Missing required option: --id');
}
parent::fire();
}
/**
* Replace the class name for the given stub.
*
* #param string $stub
* #param string $name
* #return string
*/
protected function replaceClass($stub, $name)
{
$stub = parent::replaceClass($stub, $name);
return str_replace('dummy:cron', 'Cron_' . $this->option('id'), $stub);
}
/**
* Determine if the class already exists.
*
* #param string $rawName
* #return bool
*/
protected function alreadyExists($rawName)
{
return class_exists($rawName);
}
/**
* Get the console command options.
*
* #return array
*/
protected function getOptions()
{
return [
['id', null, InputOption::VALUE_REQUIRED, 'The ID of the Cron being Generated.'],
];
}
}
I figured it out, it was my custom code that was to blame
/**
* Determine if the class already exists.
*
* #param string $rawName
* #return bool
*/
protected function alreadyExists($rawName)
{
return class_exists($rawName);
}
This was overriding the default configurations which made it fail probably because of the $rawName variable.
In my case simply removing this function solved the issue.

Resources