shell bypass 403
<?php
/**
* @package Joomla.Plugin
* @subpackage Webservices.Weblinks
*
* @copyright Copyright (C) 2005 - 2019 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace Joomla\Plugin\WebServices\Weblinks\Extension;
use Joomla\CMS\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Factory;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\ApiRouter;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* Web Services adapter for com_weblinks.
*
* @since __DEPLOY_VERSION__
*/
class Weblinks extends CMSPlugin
{
/**
* Load the language file on instantiation.
*
* @var boolean
* @since __DEPLOY_VERSION__
*/
protected $autoloadLanguage = true;
/**
* Registers com_weblinks's API's routes in the application
*
* @param ApiRouter &$router The API Routing object
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
public function onBeforeApiRoute(&$router)
{
$isPublic = $this->params->get('public', false);
$router->createCRUDRoutes(
'v1/weblinks',
'weblinks',
['component' => 'com_weblinks'],
$isPublic // <-- Only GET is public
);
$router->createCRUDRoutes(
'v1/weblinks/categories',
'categories',
['component' => 'com_categories', 'extension' => 'com_weblinks'],
$isPublic // <-- Only GET is public
);
$this->createFieldsRoutes($router, $isPublic);
}
/**
* Create fields routes
*
* @param ApiRouter &$router The API Routing object
* @param boolean $isPublic Indicates if the routes are public
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
private function createFieldsRoutes(&$router, $isPublic)
{
$router->createCRUDRoutes(
'v1/fields/weblinks',
'fields',
['component' => 'com_fields', 'context' => 'com_weblinks.weblink'],
$isPublic // <-- Only GET is public
);
$router->createCRUDRoutes(
'v1/fields/groups/weblinks',
'groups',
['component' => 'com_fields', 'context' => 'com_weblinks.weblink'],
$isPublic // <-- Only GET is public
);
}
/**
* Event handler that runs after the API router has processed the request.
*
* Applies rate limiting to public Weblinks API endpoints for guest users,
* using either non-persistent (file-based) or persistent (cache-based) strategies,
* depending on the current Joomla cache configuration.
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
public function onAfterApiRoute()
{
$app = Factory::getApplication();
$uri = $app->getInput()->server->get('REQUEST_URI', '', 'string');
$isPublic = $this->params->get('public', false);
// Only apply to weblinks-related API requests for guest users
if (
strpos($uri, '/weblinks') !== false
&&
true !== $app->login(credentials: ['username' => ''], options: ['silent' => true, 'action' => 'core.login.api'])
&&
true === $isPublic
) {
$ip = $_SERVER['REMOTE_ADDR'];
$limit = $this->params->get('max_requests', 2);
$window = $this->params->get('window_seconds', 180);
$config = Factory::getApplication()->getConfig();
$caching = (int) $config->get('caching', 0);
if ($caching === 0) {
// Non-persistent (file-based) caching
$this->applyNonPersistentRateLimit($ip, $limit, $window);
} else {
// Persistent caching
$this->applyPersistentRateLimit($ip, $limit, $window);
}
}
}
/**
* Applies rate limiting using a non-persistent file-based storage.
*
* This method stores request counts in JSON files within the site's temporary
* directory. Each file corresponds to a user's IP address.
*
* @param string $userIp The IP address of the user.
* @param int $maxRequests The maximum number of allowed requests in the time window.
* @param int $windowSeconds The time window in seconds for the rate limit.
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
private function applyNonPersistentRateLimit(string $userIp, int $maxRequests, int $windowSeconds): void
{
$storageDir = JPATH_ROOT . '/tmp/api_rate_limit/';
if (!is_dir($storageDir)) {
mkdir($storageDir, 0755, true);
}
$file = $storageDir . md5($userIp) . '.json';
// Load or initialize rate data
if (file_exists($file)) {
$rateData = json_decode(file_get_contents($file), true);
if (!\is_array($rateData)) {
$rateData = ['count' => 0, 'start' => time()];
}
} else {
$rateData = ['count' => 0, 'start' => time()];
}
// Reset rate data if the time window has passed
if (time() - $rateData['start'] > $windowSeconds) {
$rateData = ['count' => 0, 'start' => time()];
}
// Increment the request count
$rateData['count']++;
// Calculate remaining requests and reset time
$remainingRequests = $maxRequests - $rateData['count'];
$resetTime = $rateData['start'] + $windowSeconds;
// Set rate-limiting headers
$this->setRateLimitHeaders($remainingRequests, $resetTime);
// Check if the rate limit is exceeded
if ($rateData['count'] > $maxRequests) {
$retryAfter = $resetTime - time();
$this->handleRateLimitExceeded(max($retryAfter, 0));
}
// Save the updated rate data
file_put_contents($file, json_encode($rateData));
}
/**
* Applies rate limiting using Joomla's persistent cache.
*
* This method leverages Joomla's Cache to store and retrieve
* rate limit data, providing a more performant and persistent solution
* when caching is enabled on the site.
*
* @param string $userIp The IP address of the user.
* @param int $maxRequests The maximum number of allowed requests in the time window.
* @param int $windowSeconds The time window in seconds for the rate limit.
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
private function applyPersistentRateLimit(string $userIp, int $maxRequests, int $windowSeconds): void
{
// Use Joomla cache if persistent
$cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)
->createCacheController('output', [
'defaultgroup' => 'weblinks_api_rate_limit',
'lifetime' => $windowSeconds,
]);
$cacheKey = md5('api_rate_' . $userIp);
// Load or initialize rate data
$rateData = $cache->get($cacheKey);
if (!$rateData) {
$rateData = ['count' => 0, 'start' => time()];
}
// Reset rate data if the time window has passed
if (time() - $rateData['start'] > $windowSeconds) {
$rateData = ['count' => 0, 'start' => time()];
}
// Increment the request count
$rateData['count']++;
// Calculate remaining requests and reset time
$remainingRequests = $maxRequests - $rateData['count'];
$resetTime = $rateData['start'] + $windowSeconds;
// Set rate-limiting headers
$this->setRateLimitHeaders($remainingRequests, $resetTime);
// Check if the rate limit is exceeded
if ($rateData['count'] > $maxRequests) {
$retryAfter = $resetTime - time();
$this->handleRateLimitExceeded(max($retryAfter, 0));
}
// Save the updated rate data
$cache->store($rateData, $cacheKey);
}
/**
* Handles the response when a user exceeds the API rate limit.
*
* This method sends an HTTP 429 "Too Many Requests" response along with a "Retry-After" header.
*
* @param int $retryAfterSeconds The number of seconds after which the client can retry.
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
private function handleRateLimitExceeded(int $retryAfterSeconds): void
{
// Customize the behavior here (e.g., log the event, return a response, etc.)
http_response_code(429); // HTTP 429 Too Many Requests
$retryTime = gmdate('D, d M Y H:i:s', time() + $retryAfterSeconds) . ' GMT';
header('Retry-After: ' . $retryTime);
echo json_encode([
'errors' => [
[
'title' => 'Rate limit exceeded',
'code' => 429,
],
],
]);
exit;
}
/**
* Sets the rate limit headers on the response.
*
* @param int $remaining The number of requests remaining in the window.
* @param int $resetTime The Unix timestamp when the rate limit window resets.
*
* @return void
*
* @since __DEPLOY_VERSION__
*/
private function setRateLimitHeaders(int $remainingRequests, int $resetTime): void
{
header('X-RateLimit-Remaining: ' . max($remainingRequests, 0));
header('X-RateLimit-Reset: ' . $resetTime - time());
}
}