Skip to content

Commit

Permalink
Added mew middleware methods to secure Leantime
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelfolaron committed Apr 14, 2024
1 parent 3d4194c commit cd79ec1
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 5 deletions.
5 changes: 5 additions & 0 deletions app/Core/DefaultConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -471,4 +471,9 @@ class DefaultConfig
* @var string Redis URL
*/
public string $redisUrl = '';

/**
* @var string trusted Proxies
*/
public string $trustedProxies = '127.0.0.1,REMOTE_ADDR';
}
2 changes: 1 addition & 1 deletion app/Core/Frontcontroller.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ public static function getModuleName(string $completeName = null): string


/**
* getCurrentRoute - gets the current main action
* getCurrentRoute - gets the current main action in format module.action
*
* @access public
* @return string
Expand Down
2 changes: 2 additions & 0 deletions app/Core/HttpKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,11 @@ public function getApplication(): \Leantime\Core\Application
public function getMiddleware(): array
{
return self::dispatch_filter('http_middleware', [
Middleware\TrustProxies::class,
Middleware\InitialHeaders::class,
Middleware\Installed::class,
Middleware\Updated::class,
Middleware\RequestRateLimiter::class,
app()->make(IncomingRequest::class) instanceof ApiRequest
? Middleware\ApiAuth::class
: Middleware\Auth::class,
Expand Down
2 changes: 1 addition & 1 deletion app/Core/IncomingRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ public function getFullUrl(): string
public function getRequestUri(): string
{


$requestUri = parent::getRequestUri();

$config = app()->make(Environment::class);
Expand Down Expand Up @@ -134,4 +133,5 @@ public function getRequestParams(string $method = null): array
default => $this->query->all(),
};
}

}
1 change: 1 addition & 0 deletions app/Core/Middleware/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Auth
"errors.error404",
"errors.error500",
"api.i18n",
"api.static-asset",
"calendar.ical",
"oidc.login",
"oidc.callback",
Expand Down
16 changes: 15 additions & 1 deletion app/Core/Middleware/InitialHeaders.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,28 @@ public function handle(IncomingRequest $request, Closure $next): Response
{
$response = $next($request);

//Content Security Policy
$cspParts = [
"default-src 'self' 'unsafe-inline'",
"base-uri 'self';",
"script-src 'self' 'unsafe-inline' unpkg.com",
"font-src 'self' data:",
"img-src 'self' *.leantime.io data: blob:",
"frame-src 'self' *.google.com *.microsoft.com *.live.com",
"frame-ancestors 'self' *.google.com *.microsoft.com *.live.com",
];
$csp = implode(";", $cspParts);

foreach (
self::dispatch_filter('headers', [
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy', 'same-origin',
'Access-Control-Allow-Origin' => BASE_URL,
'Cache-Control' => 'no-cache, no-store, must-revalidate',
'Pragma' => 'no-cache'
'Pragma' => 'no-cache',
'Content-Security-Policy' => $csp,
]) as $key => $value
) {
if ($response->headers->has($key)) {
Expand Down
92 changes: 92 additions & 0 deletions app/Core/Middleware/RequestRateLimiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

namespace Leantime\Core\Middleware;

use Closure;
use Illuminate\Cache\RateLimiter;
use Leantime\Core\ApiRequest;
use Leantime\Core\Eventhelpers;
use Leantime\Core\Frontcontroller;
use Leantime\Core\IncomingRequest;
use Leantime\Core\Middleware\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* Class ApiRateLimiter
*
* This class is responsible for rate limiting requests, login requests and api requests
*/
class RequestRateLimiter
{
use Eventhelpers;

protected RateLimiter $limiter;

/**
* __construct
* Constructor method for the class.
*
* @param RateLimiter $limiter The RateLimiter object to be initialized.
* @return void.
*/
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}

/**
* Handle the incoming request.
*
* @param IncomingRequest $request The incoming request object.
* @param Closure $next The next middleware closure.
* @return Response The response object.
*/
public function handle(IncomingRequest $request, Closure $next): Response
{

//Key
$key = $request->getClientIp();

//General Limit per minute
$limit = 1000;

//API Routes Limit
if ($request instanceof ApiRequest) {
$apiKey = "";
$key = app()->make(ApiRequest::class)->getAPIKeyUser($apiKey);
$limit = 10;
}

$route = Frontcontroller::getCurrentRoute();

if ($route == "auth.login") {
$limit = 20;
$key = $key . ".loginAttempts";
}

$key = self::dispatch_filter(
"rateLimit",
$key,
[
"bootloader" => $this,
],
);

$limit = self::dispatch_filter(
"rateLimit",
$limit,
[
"bootloader" => $this,
"key" => $key,
],
);

if ($this->limiter->tooManyAttempts($key, $limit)) {
error_log("too many requests per minute: " . $key);
return new Response(json_encode(['error' => 'Too many requests per minute.']), Response::HTTP_TOO_MANY_REQUESTS);
}
$this->limiter->hit($key, 60);

return $next($request);
}
}
83 changes: 83 additions & 0 deletions app/Core/Middleware/TrustProxies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Leantime\Core\Middleware;

use Closure;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Leantime\Core\ApiRequest;
use Leantime\Core\Environment;
use Leantime\Core\Eventhelpers;
use Leantime\Core\Frontcontroller;
use Leantime\Core\IncomingRequest;
use Leantime\Core\Middleware\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* Class TrustProxies
*
* The TrustProxies class is responsible for handling incoming requests and checking if they are from trusted proxies.
*
* @package Your\Namespace
*/
class TrustProxies
{
use Eventhelpers;

/**
* The trusted proxies for this application.
*
* @var array
*/
protected $proxies = [];

/**
* The headers that should be used to detect proxies.
*
* @var string
*/
protected $headers = IncomingRequest::HEADER_X_FORWARDED_FOR |
IncomingRequest::HEADER_X_FORWARDED_HOST |
IncomingRequest::HEADER_X_FORWARDED_PORT |
IncomingRequest::HEADER_X_FORWARDED_PROTO |
IncomingRequest::HEADER_X_FORWARDED_AWS_ELB;

/**
* Constructor for the class.
*
* @param Environment $config An instance of the Environment class.
*/
public function __construct(Environment $config)
{

if (empty($config->trustedProxies)) {
$config->trustedProxies = "127.0.0.1,REMOTE_ADDR";
}

$this->proxies = self::dispatch_filter(
"trustedProxies",
explode(",", $config->trustedProxies),
['bootloader' => $this]
);

IncomingRequest::setTrustedProxies($this->proxies, $this->headers);
}

/**
* Handle the incoming request and pass it to the next middleware.
* If the request is not from a trusted proxy, it returns a response with an error message.
*
* @param IncomingRequest $request The incoming request.
* @param Closure $next The next middleware closure.
* @return Response The response returned by the next middleware.
*/
public function handle(IncomingRequest $request, Closure $next): Response
{

if (!$request->isFromTrustedProxy()) {
return new Response(json_encode(['error' => 'Not a trusted proxy']), 403);
}

return $next($request);
}
}
2 changes: 1 addition & 1 deletion config/sample.env
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ LEAN_DB_PASSWORD = '' # Database password
LEAN_DB_DATABASE = '' # Database name
LEAN_DB_PORT = '3306' # Database port


## Optional Configuration, you may omit these from your .env file

## Default Settings
Expand All @@ -26,6 +25,7 @@ LEAN_SESSION_PASSWORD = '3evBlq9zdUEuzKvVJHWWx3QzsQhturBApxwcws2m' # Salting se
LEAN_SESSION_EXPIRATION = 28800 # How many seconds after inactivity should we logout? 28800seconds = 8hours
LEAN_LOG_PATH = '' # Default Log Path (including filename), if not set /logs/error.log will be used
LEAN_DISABLE_LOGIN_FORM = false # If true then don't show the login form (useful only if additional auth method[s] are available)
#LEAN_TRUSTED_PROXIES = '127.0.0.1,REMOTE_ADDR' # Set trusted proxy ips. Can be used to restrict access or define access from certain proxies only. Default is access from anywhere

## Look & Feel, these settings are available in the UI and can be overwritten there.
LEAN_LOGO_PATH = '/dist/images/logo.svg' # Default logo path, can be changed later
Expand Down
1 change: 0 additions & 1 deletion webpack.mix.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ require('mix-tailwindcss');
require('dotenv').config({ path: 'config/.env' });

mix
.sourceMaps(true, 'source-map')
.setPublicPath('public/dist') // this is the URL to place assets referenced in the CSS/JS
.setResourceRoot(`../`) // this is what to prefix the URL with
.combine('./public/assets/js/libs/prism/prism.js', `public/dist/js/compiled-footer.${version}.min.js`)
Expand Down

0 comments on commit cd79ec1

Please sign in to comment.