- Add authentication failure logging to AuthMiddleware (session expiry, access denied, unauthenticated access attempts) - Add UrlHelper for secure URL generation with host validation against configurable ALLOWED_HOSTS whitelist - Add OutputHelper with consistent XSS-safe escaping functions (h, attr, json, url, css, truncate, date, cssClass) - Add validation to AuditLogModel query parameters (pagination limits, date format validation, action/entity type validation, IP sanitization) - Add APP_DOMAIN and ALLOWED_HOSTS configuration options Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
100 lines
2.8 KiB
PHP
100 lines
2.8 KiB
PHP
<?php
|
|
/**
|
|
* UrlHelper - URL and domain utilities
|
|
*
|
|
* Provides secure URL generation with host validation.
|
|
*/
|
|
class UrlHelper {
|
|
/**
|
|
* Get the application base URL with validated host
|
|
*
|
|
* Uses APP_DOMAIN from config if set, otherwise validates HTTP_HOST
|
|
* against ALLOWED_HOSTS whitelist.
|
|
*
|
|
* @return string Base URL (e.g., "https://example.com")
|
|
*/
|
|
public static function getBaseUrl(): string {
|
|
$protocol = self::getProtocol();
|
|
$host = self::getValidatedHost();
|
|
|
|
return "{$protocol}://{$host}";
|
|
}
|
|
|
|
/**
|
|
* Get the current protocol (http or https)
|
|
*
|
|
* @return string 'https' or 'http'
|
|
*/
|
|
public static function getProtocol(): string {
|
|
if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
|
|
return 'https';
|
|
}
|
|
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
|
|
return 'https';
|
|
}
|
|
if (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) {
|
|
return 'https';
|
|
}
|
|
return 'http';
|
|
}
|
|
|
|
/**
|
|
* Get validated hostname
|
|
*
|
|
* Priority:
|
|
* 1. APP_DOMAIN from config (if set)
|
|
* 2. HTTP_HOST if it passes validation
|
|
* 3. First allowed host as fallback
|
|
*
|
|
* @return string Validated hostname
|
|
*/
|
|
public static function getValidatedHost(): string {
|
|
$config = $GLOBALS['config'] ?? [];
|
|
|
|
// Use configured APP_DOMAIN if available
|
|
if (!empty($config['APP_DOMAIN'])) {
|
|
return $config['APP_DOMAIN'];
|
|
}
|
|
|
|
// Get allowed hosts
|
|
$allowedHosts = $config['ALLOWED_HOSTS'] ?? ['localhost'];
|
|
|
|
// Validate HTTP_HOST against whitelist
|
|
$httpHost = $_SERVER['HTTP_HOST'] ?? '';
|
|
|
|
// Strip port if present for comparison
|
|
$hostWithoutPort = preg_replace('/:\d+$/', '', $httpHost);
|
|
|
|
if (in_array($hostWithoutPort, $allowedHosts, true)) {
|
|
return $httpHost;
|
|
}
|
|
|
|
// Log suspicious host header
|
|
if (!empty($httpHost) && $httpHost !== 'localhost') {
|
|
error_log("UrlHelper: Rejected HTTP_HOST '{$httpHost}' - not in allowed hosts");
|
|
}
|
|
|
|
// Return first allowed host as fallback
|
|
return $allowedHosts[0] ?? 'localhost';
|
|
}
|
|
|
|
/**
|
|
* Build a full URL for a ticket
|
|
*
|
|
* @param string $ticketId Ticket ID
|
|
* @return string Full ticket URL
|
|
*/
|
|
public static function ticketUrl(string $ticketId): string {
|
|
return self::getBaseUrl() . '/ticket/' . urlencode($ticketId);
|
|
}
|
|
|
|
/**
|
|
* Check if the current request is using HTTPS
|
|
*
|
|
* @return bool True if HTTPS
|
|
*/
|
|
public static function isSecure(): bool {
|
|
return self::getProtocol() === 'https';
|
|
}
|
|
}
|