mirror of
https://github.com/manualdousuario/marreta.git
synced 2025-09-01 10:10:14 +00:00
headers migrados para rotas e padronização de sanitização de urls
This commit is contained in:
parent
7013b56b2f
commit
ab2e596621
4 changed files with 103 additions and 111 deletions
|
@ -234,33 +234,27 @@ class URLAnalyzer
|
||||||
// Reset das regras ativadas para nova análise
|
// Reset das regras ativadas para nova análise
|
||||||
$this->activatedRules = [];
|
$this->activatedRules = [];
|
||||||
|
|
||||||
// 1. Clean URL / Limpa a URL
|
// 1. Check cache / Verifica cache
|
||||||
$cleanUrl = $this->cleanUrl($url);
|
if ($this->cache->exists($url)) {
|
||||||
if (!$cleanUrl) {
|
return $this->cache->get($url);
|
||||||
$this->throwError(self::ERROR_INVALID_URL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check cache / Verifica cache
|
// 2. Check blocked domains / Verifica domínios bloqueados
|
||||||
if ($this->cache->exists($cleanUrl)) {
|
$host = parse_url($url, PHP_URL_HOST);
|
||||||
return $this->cache->get($cleanUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check blocked domains / Verifica domínios bloqueados
|
|
||||||
$host = parse_url($cleanUrl, PHP_URL_HOST);
|
|
||||||
if (!$host) {
|
if (!$host) {
|
||||||
$this->throwError(self::ERROR_INVALID_URL);
|
$this->throwError(self::ERROR_INVALID_URL);
|
||||||
}
|
}
|
||||||
$host = preg_replace('/^www\./', '', $host);
|
$host = preg_replace('/^www\./', '', $host);
|
||||||
|
|
||||||
if (in_array($host, BLOCKED_DOMAINS)) {
|
if (in_array($host, BLOCKED_DOMAINS)) {
|
||||||
Logger::getInstance()->logUrl($cleanUrl, 'BLOCKED_DOMAIN');
|
Logger::getInstance()->logUrl($url, 'BLOCKED_DOMAIN');
|
||||||
$this->throwError(self::ERROR_BLOCKED_DOMAIN);
|
$this->throwError(self::ERROR_BLOCKED_DOMAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check URL status code before proceeding
|
// 3. Check URL status code before proceeding
|
||||||
$redirectInfo = $this->checkStatus($cleanUrl);
|
$redirectInfo = $this->checkStatus($url);
|
||||||
if ($redirectInfo['httpCode'] !== 200) {
|
if ($redirectInfo['httpCode'] !== 200) {
|
||||||
Logger::getInstance()->logUrl($cleanUrl, 'INVALID_STATUS_CODE', "HTTP {$redirectInfo['httpCode']}");
|
Logger::getInstance()->logUrl($url, 'INVALID_STATUS_CODE', "HTTP {$redirectInfo['httpCode']}");
|
||||||
if ($redirectInfo['httpCode'] === 404) {
|
if ($redirectInfo['httpCode'] === 404) {
|
||||||
$this->throwError(self::ERROR_NOT_FOUND);
|
$this->throwError(self::ERROR_NOT_FOUND);
|
||||||
} else {
|
} else {
|
||||||
|
@ -279,33 +273,33 @@ class URLAnalyzer
|
||||||
$content = null;
|
$content = null;
|
||||||
switch ($fetchStrategy) {
|
switch ($fetchStrategy) {
|
||||||
case 'fetchContent':
|
case 'fetchContent':
|
||||||
$content = $this->fetchContent($cleanUrl);
|
$content = $this->fetchContent($url);
|
||||||
break;
|
break;
|
||||||
case 'fetchFromWaybackMachine':
|
case 'fetchFromWaybackMachine':
|
||||||
$content = $this->fetchFromWaybackMachine($cleanUrl);
|
$content = $this->fetchFromWaybackMachine($url);
|
||||||
break;
|
break;
|
||||||
case 'fetchFromSelenium':
|
case 'fetchFromSelenium':
|
||||||
$content = $this->fetchFromSelenium($cleanUrl, isset($domainRules['browser']) ? $domainRules['browser'] : 'firefox');
|
$content = $this->fetchFromSelenium($url, isset($domainRules['browser']) ? $domainRules['browser'] : 'firefox');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($content)) {
|
if (!empty($content)) {
|
||||||
$this->activatedRules[] = "fetchStrategy: $fetchStrategy";
|
$this->activatedRules[] = "fetchStrategy: $fetchStrategy";
|
||||||
$processedContent = $this->processContent($content, $host, $cleanUrl);
|
$processedContent = $this->processContent($content, $host, $url);
|
||||||
$this->cache->set($cleanUrl, $processedContent);
|
$this->cache->set($url, $processedContent);
|
||||||
return $processedContent;
|
return $processedContent;
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Logger::getInstance()->logUrl($cleanUrl, strtoupper($fetchStrategy) . '_ERROR', $e->getMessage());
|
Logger::getInstance()->logUrl($url, strtoupper($fetchStrategy) . '_ERROR', $e->getMessage());
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Try all strategies in sequence
|
// 5. Try all strategies in sequence
|
||||||
$fetchStrategies = [
|
$fetchStrategies = [
|
||||||
['method' => 'fetchContent', 'args' => [$cleanUrl]],
|
['method' => 'fetchContent', 'args' => [$url]],
|
||||||
['method' => 'fetchFromWaybackMachine', 'args' => [$cleanUrl]],
|
['method' => 'fetchFromWaybackMachine', 'args' => [$url]],
|
||||||
['method' => 'fetchFromSelenium', 'args' => [$cleanUrl, 'firefox']]
|
['method' => 'fetchFromSelenium', 'args' => [$url, 'firefox']]
|
||||||
];
|
];
|
||||||
|
|
||||||
$lastError = null;
|
$lastError = null;
|
||||||
|
@ -314,8 +308,8 @@ class URLAnalyzer
|
||||||
$content = call_user_func_array([$this, $strategy['method']], $strategy['args']);
|
$content = call_user_func_array([$this, $strategy['method']], $strategy['args']);
|
||||||
if (!empty($content)) {
|
if (!empty($content)) {
|
||||||
$this->activatedRules[] = "fetchStrategy: {$strategy['method']}";
|
$this->activatedRules[] = "fetchStrategy: {$strategy['method']}";
|
||||||
$processedContent = $this->processContent($content, $host, $cleanUrl);
|
$processedContent = $this->processContent($content, $host, $url);
|
||||||
$this->cache->set($cleanUrl, $processedContent);
|
$this->cache->set($url, $processedContent);
|
||||||
return $processedContent;
|
return $processedContent;
|
||||||
}
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
@ -326,7 +320,7 @@ class URLAnalyzer
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, all strategies failed
|
// If we get here, all strategies failed
|
||||||
Logger::getInstance()->logUrl($cleanUrl, 'GENERAL_FETCH_ERROR');
|
Logger::getInstance()->logUrl($url, 'GENERAL_FETCH_ERROR');
|
||||||
if ($lastError) {
|
if ($lastError) {
|
||||||
$message = $lastError->getMessage();
|
$message = $lastError->getMessage();
|
||||||
if (strpos($message, 'DNS') !== false) {
|
if (strpos($message, 'DNS') !== false) {
|
||||||
|
@ -432,8 +426,8 @@ class URLAnalyzer
|
||||||
*/
|
*/
|
||||||
private function fetchFromWaybackMachine($url)
|
private function fetchFromWaybackMachine($url)
|
||||||
{
|
{
|
||||||
$cleanUrl = preg_replace('#^https?://#', '', $url);
|
$url = preg_replace('#^https?://#', '', $url);
|
||||||
$availabilityUrl = "https://archive.org/wayback/available?url=" . urlencode($cleanUrl);
|
$availabilityUrl = "https://archive.org/wayback/available?url=" . urlencode($url);
|
||||||
|
|
||||||
$curl = new Curl();
|
$curl = new Curl();
|
||||||
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
|
$curl->setOpt(CURLOPT_FOLLOWLOCATION, true);
|
||||||
|
@ -552,36 +546,6 @@ class URLAnalyzer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean and normalize a URL
|
|
||||||
* Limpa e normaliza uma URL
|
|
||||||
*/
|
|
||||||
private function cleanUrl($url)
|
|
||||||
{
|
|
||||||
$url = trim($url);
|
|
||||||
|
|
||||||
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preg_match('#https://([^.]+)\.cdn\.ampproject\.org/v/s/([^/]+)(.*)#', $url, $matches)) {
|
|
||||||
$url = 'https://' . $matches[2] . $matches[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = parse_url($url);
|
|
||||||
if (!isset($parts['scheme']) || !isset($parts['host'])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$cleanedUrl = $parts['scheme'] . '://' . $parts['host'];
|
|
||||||
|
|
||||||
if (isset($parts['path'])) {
|
|
||||||
$cleanedUrl .= $parts['path'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cleanedUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get specific rules for a domain
|
* Get specific rules for a domain
|
||||||
* Obtém regras específicas para um domínio
|
* Obtém regras específicas para um domínio
|
||||||
|
|
|
@ -50,21 +50,19 @@ class Router
|
||||||
$message_type = '';
|
$message_type = '';
|
||||||
$url = '';
|
$url = '';
|
||||||
|
|
||||||
// Processa mensagens da query string
|
// Sanitize and process query string messages
|
||||||
// Process query string messages
|
|
||||||
if (isset($_GET['message'])) {
|
if (isset($_GET['message'])) {
|
||||||
$message_key = $_GET['message'];
|
$message_key = htmlspecialchars(trim($_GET['message']), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
$messageData = \Language::getMessage($message_key);
|
$messageData = \Language::getMessage($message_key);
|
||||||
$message = $messageData['message'];
|
$message = htmlspecialchars($messageData['message'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
$message_type = $messageData['type'];
|
$message_type = htmlspecialchars($messageData['type'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processa submissão do formulário
|
|
||||||
// Process form submission
|
// Process form submission
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['url'])) {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['url'])) {
|
||||||
$url = filter_var($_POST['url'], FILTER_SANITIZE_URL);
|
$url = $this->sanitizeUrl($_POST['url']);
|
||||||
if (filter_var($url, FILTER_VALIDATE_URL)) {
|
if (filter_var($url, FILTER_VALIDATE_URL)) {
|
||||||
header('Location: ' . SITE_URL . '/p/' . urlencode($url));
|
header('Location: ' . SITE_URL . '/p/' . $url);
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
$messageData = \Language::getMessage('INVALID_URL');
|
$messageData = \Language::getMessage('INVALID_URL');
|
||||||
|
@ -84,7 +82,7 @@ class Router
|
||||||
// Rota da API - usa URLProcessor em modo API
|
// Rota da API - usa URLProcessor em modo API
|
||||||
// API route - uses URLProcessor in API mode
|
// API route - uses URLProcessor in API mode
|
||||||
$r->addRoute('GET', '/api/{url:.+}', function($vars) {
|
$r->addRoute('GET', '/api/{url:.+}', function($vars) {
|
||||||
$processor = new URLProcessor($vars['url'], true);
|
$processor = new URLProcessor($this->sanitizeUrl($vars['url']), true);
|
||||||
$processor->process();
|
$processor->process();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -98,23 +96,23 @@ class Router
|
||||||
// Rota de processamento - usa URLProcessor em modo web
|
// Rota de processamento - usa URLProcessor em modo web
|
||||||
// Processing route - uses URLProcessor in web mode
|
// Processing route - uses URLProcessor in web mode
|
||||||
$r->addRoute('GET', '/p/{url:.+}', function($vars) {
|
$r->addRoute('GET', '/p/{url:.+}', function($vars) {
|
||||||
$processor = new URLProcessor($vars['url'], false);
|
$processor = new URLProcessor($this->sanitizeUrl($vars['url']), false);
|
||||||
$processor->process();
|
$processor->process();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rota de processamento com query parameter ou sem parâmetros
|
|
||||||
// Processing route with query parameter or without parameters
|
// Processing route with query parameter or without parameters
|
||||||
$r->addRoute('GET', '/p[/]', function() {
|
$r->addRoute('GET', '/p[/]', function() {
|
||||||
if (isset($_GET['url']) || isset($_GET['text'])) {
|
if (isset($_GET['url']) || isset($_GET['text'])) {
|
||||||
$url = isset($_GET['url']) ? $_GET['url'] : '';
|
// Sanitize input parameters
|
||||||
$text = isset($_GET['text']) ? $_GET['text'] : '';
|
$url = isset($_GET['url']) ? $this->sanitizeUrl($_GET['url']) : '';
|
||||||
|
$text = isset($_GET['text']) ? $this->sanitizeUrl($_GET['text']) : '';
|
||||||
|
|
||||||
// Check which parameter is a valid URL
|
// Check which parameter is a valid URL
|
||||||
if (filter_var($url, FILTER_VALIDATE_URL)) {
|
if (filter_var($url, FILTER_VALIDATE_URL)) {
|
||||||
header('Location: /p/' . urlencode($url));
|
header('Location: /p/' . $url);
|
||||||
exit;
|
exit;
|
||||||
} elseif (filter_var($text, FILTER_VALIDATE_URL)) {
|
} elseif (filter_var($text, FILTER_VALIDATE_URL)) {
|
||||||
header('Location: /p/' . urlencode($text));
|
header('Location: /p/' . $text);
|
||||||
exit;
|
exit;
|
||||||
} else {
|
} else {
|
||||||
header('Location: /?message=INVALID_URL');
|
header('Location: /?message=INVALID_URL');
|
||||||
|
@ -134,11 +132,73 @@ class Router
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Despacha a requisição para a rota apropriada
|
* Sanitizes URLs to prevent XSS and injection attacks
|
||||||
* Dispatches the request to the appropriate route
|
* Sanitiza URLs para prevenir ataques XSS e injeções
|
||||||
|
*
|
||||||
|
* @param string $url The URL to sanitize
|
||||||
|
* @return string The sanitized URL
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Sanitizes and normalizes URLs
|
||||||
|
* Sanitiza e normaliza URLs
|
||||||
|
*
|
||||||
|
* @param string $url The URL to sanitize and normalize
|
||||||
|
* @return string|false The cleaned URL or false if invalid
|
||||||
|
*/
|
||||||
|
private function sanitizeUrl(string $url): string
|
||||||
|
{
|
||||||
|
$url = trim($url);
|
||||||
|
|
||||||
|
// Basic URL validation
|
||||||
|
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle AMP URLs
|
||||||
|
if (preg_match('#https://([^.]+)\.cdn\.ampproject\.org/v/s/([^/]+)(.*)#', $url, $matches)) {
|
||||||
|
$url = 'https://' . $matches[2] . $matches[3];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and reconstruct URL to ensure proper structure
|
||||||
|
$parts = parse_url($url);
|
||||||
|
if (!isset($parts['scheme']) || !isset($parts['host'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$cleanedUrl = $parts['scheme'] . '://' . $parts['host'];
|
||||||
|
|
||||||
|
if (isset($parts['path'])) {
|
||||||
|
$cleanedUrl .= $parts['path'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove control characters and sanitize
|
||||||
|
$cleanedUrl = preg_replace('/[\x00-\x1F\x7F]/', '', $cleanedUrl);
|
||||||
|
$cleanedUrl = filter_var($cleanedUrl, FILTER_SANITIZE_URL);
|
||||||
|
|
||||||
|
// Convert special characters to HTML entities
|
||||||
|
return htmlspecialchars($cleanedUrl, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets security headers for all responses
|
||||||
|
* Define cabeçalhos de segurança para todas as respostas
|
||||||
|
*/
|
||||||
|
private function setSecurityHeaders()
|
||||||
|
{
|
||||||
|
// Set security headers
|
||||||
|
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:;");
|
||||||
|
header("X-Content-Type-Options: nosniff");
|
||||||
|
header("X-Frame-Options: DENY");
|
||||||
|
header("X-XSS-Protection: 1; mode=block");
|
||||||
|
header("Referrer-Policy: strict-origin-when-cross-origin");
|
||||||
|
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
|
||||||
|
header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
|
||||||
|
}
|
||||||
|
|
||||||
public function dispatch()
|
public function dispatch()
|
||||||
{
|
{
|
||||||
|
$this->setSecurityHeaders();
|
||||||
|
|
||||||
$httpMethod = $_SERVER['REQUEST_METHOD'];
|
$httpMethod = $_SERVER['REQUEST_METHOD'];
|
||||||
$uri = $_SERVER['REQUEST_URI'];
|
$uri = $_SERVER['REQUEST_URI'];
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ class URLProcessor
|
||||||
require_once __DIR__ . '/../inc/URLAnalyzer.php';
|
require_once __DIR__ . '/../inc/URLAnalyzer.php';
|
||||||
require_once __DIR__ . '/../inc/Language.php';
|
require_once __DIR__ . '/../inc/Language.php';
|
||||||
|
|
||||||
$this->url = urldecode($url);
|
$this->url = $url;
|
||||||
$this->isApi = $isApi;
|
$this->isApi = $isApi;
|
||||||
$this->analyzer = new \URLAnalyzer();
|
$this->analyzer = new \URLAnalyzer();
|
||||||
|
|
||||||
|
@ -82,20 +82,6 @@ class URLProcessor
|
||||||
*/
|
*/
|
||||||
public function process(): void
|
public function process(): void
|
||||||
{
|
{
|
||||||
// Validate URL format
|
|
||||||
if (!filter_var($this->url, FILTER_VALIDATE_URL)) {
|
|
||||||
if ($this->isApi) {
|
|
||||||
$this->sendApiResponse([
|
|
||||||
'error' => [
|
|
||||||
'type' => \URLAnalyzer::ERROR_INVALID_URL,
|
|
||||||
'message' => \Language::getMessage('INVALID_URL')['message']
|
|
||||||
]
|
|
||||||
], 400);
|
|
||||||
} else {
|
|
||||||
$this->redirect(SITE_URL, \URLAnalyzer::ERROR_INVALID_URL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check for redirects in web mode
|
// Check for redirects in web mode
|
||||||
if (!$this->isApi) {
|
if (!$this->isApi) {
|
||||||
|
|
22
default.conf
22
default.conf
|
@ -11,26 +11,8 @@ server {
|
||||||
# Oculta a versão do NGINX para reduzir informações expostas
|
# Oculta a versão do NGINX para reduzir informações expostas
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
# Security Headers / Cabeçalhos de Segurança
|
# NGINX-specific security configurations
|
||||||
# Enable HSTS (HTTP Strict Transport Security) to force HTTPS connections
|
# Configurações de segurança específicas do NGINX
|
||||||
# Habilita HSTS (HTTP Strict Transport Security) para forçar conexões HTTPS
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
||||||
|
|
||||||
# Prevent clickjacking attacks by allowing the site to be displayed only in its own domain
|
|
||||||
# Previne ataques de clickjacking, permitindo que o site seja exibido apenas em seu próprio domínio
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
|
|
||||||
# Enable protection against Cross-Site Scripting (XSS) attacks
|
|
||||||
# Ativa proteção contra ataques de Cross-Site Scripting (XSS)
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
|
|
||||||
# Prevent browsers from MIME-type sniffing
|
|
||||||
# Impede que navegadores tentem adivinhar (sniff) o tipo MIME dos arquivos
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
|
|
||||||
# Control how referrer headers are sent
|
|
||||||
# Controla como os cabeçalhos de referência são enviados
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
|
|
||||||
# Limit upload size to prevent denial of service attacks
|
# Limit upload size to prevent denial of service attacks
|
||||||
# Limita o tamanho de uploads para prevenir ataques de negação de serviço
|
# Limita o tamanho de uploads para prevenir ataques de negação de serviço
|
||||||
|
|
Loading…
Add table
Reference in a new issue