asd
This commit is contained in:
commit
3e4a056b14
67 files changed
+13926
No files matched your search
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Security baseline, loaded first by every web entry point (via helpers.php):
|
||||
* error handling, request-method guard, security headers, HTTPS detection,
|
||||
* and hardened session settings.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
function security_config(): array
|
||||
{
|
||||
return (app_config()['security'] ?? []) + [
|
||||
'force_https' => false,
|
||||
'trusted_proxies' => [],
|
||||
'setup_key' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/** True when the client reached us over HTTPS (directly or via a trusted reverse proxy). */
|
||||
function is_https(): bool
|
||||
{
|
||||
if (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off') {
|
||||
return true;
|
||||
}
|
||||
if ((int)($_SERVER['SERVER_PORT'] ?? 0) === 443) {
|
||||
return true;
|
||||
}
|
||||
$proxies = security_config()['trusted_proxies'];
|
||||
if ($proxies && in_array($_SERVER['REMOTE_ADDR'] ?? '', $proxies, true)) {
|
||||
return strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Client IP; X-Forwarded-For is honoured only when the request came from a trusted proxy. */
|
||||
function client_ip(): string
|
||||
{
|
||||
$remote = (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
|
||||
$proxies = security_config()['trusted_proxies'];
|
||||
if ($proxies && in_array($remote, $proxies, true) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$chain = array_map('trim', explode(',', (string)$_SERVER['HTTP_X_FORWARDED_FOR']));
|
||||
// right-most address that is not one of our proxies
|
||||
foreach (array_reverse($chain) as $ip) {
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP) && !in_array($ip, $proxies, true)) {
|
||||
return substr($ip, 0, 45);
|
||||
}
|
||||
}
|
||||
}
|
||||
return substr($remote, 0, 45);
|
||||
}
|
||||
|
||||
/** Per-request nonce for the few inline scripts (Content-Security-Policy). */
|
||||
function csp_nonce(): string
|
||||
{
|
||||
static $nonce = null;
|
||||
if ($nonce === null) {
|
||||
$nonce = rtrim(strtr(base64_encode(random_bytes(18)), '+/', '-_'), '=');
|
||||
}
|
||||
return $nonce;
|
||||
}
|
||||
|
||||
function send_security_headers(bool $sensitive = false): void
|
||||
{
|
||||
if (headers_sent()) {
|
||||
return;
|
||||
}
|
||||
header_remove('X-Powered-By');
|
||||
$csp = [
|
||||
"default-src 'none'",
|
||||
"script-src 'self' 'nonce-" . csp_nonce() . "'",
|
||||
"style-src 'self'",
|
||||
"img-src 'self'",
|
||||
"font-src 'self'",
|
||||
"connect-src 'self'",
|
||||
"manifest-src 'self'",
|
||||
"form-action 'self'",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'none'",
|
||||
"object-src 'none'",
|
||||
];
|
||||
if (is_https()) {
|
||||
$csp[] = 'upgrade-insecure-requests';
|
||||
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
|
||||
}
|
||||
header('Content-Security-Policy: ' . implode('; ', $csp));
|
||||
header('X-Frame-Options: DENY');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Referrer-Policy: same-origin');
|
||||
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()');
|
||||
header('Cross-Origin-Opener-Policy: same-origin');
|
||||
header('Cross-Origin-Resource-Policy: same-origin');
|
||||
header('X-Permitted-Cross-Domain-Policies: none');
|
||||
// Pages always revalidate so they reference the newest versioned assets;
|
||||
// admin pages are never stored at all.
|
||||
header($sensitive ? 'Cache-Control: no-store, max-age=0' : 'Cache-Control: no-cache, must-revalidate');
|
||||
if ($sensitive) {
|
||||
header('Pragma: no-cache');
|
||||
}
|
||||
}
|
||||
|
||||
function security_bootstrap(): void
|
||||
{
|
||||
static $done = false;
|
||||
if ($done || PHP_SAPI === 'cli') {
|
||||
return;
|
||||
}
|
||||
$done = true;
|
||||
|
||||
// Never leak errors or stack traces to the client.
|
||||
ini_set('display_errors', '0');
|
||||
ini_set('display_startup_errors', '0');
|
||||
ini_set('log_errors', '1');
|
||||
ini_set('expose_php', '0');
|
||||
error_reporting(E_ALL);
|
||||
set_exception_handler(function (Throwable $e): void {
|
||||
error_log('[serubi] ' . get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||
if (!headers_sent()) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
}
|
||||
echo '<!doctype html><meta charset="utf-8"><title>Terjadi kesalahan</title><p>Terjadi kesalahan pada server. Silakan coba lagi beberapa saat lagi.</p>';
|
||||
});
|
||||
|
||||
// Hardened session settings (before any session_start()).
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.use_only_cookies', '1');
|
||||
ini_set('session.use_trans_sid', '0');
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
ini_set('session.sid_length', '48');
|
||||
ini_set('session.sid_bits_per_character', '5');
|
||||
ini_set('session.gc_maxlifetime', '28800');
|
||||
|
||||
// Only GET, HEAD and POST are meaningful here (blocks TRACE/PUT/DELETE/OPTIONS...).
|
||||
$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||
if (!in_array($method, ['GET', 'HEAD', 'POST'], true)) {
|
||||
header('Allow: GET, HEAD, POST');
|
||||
http_response_code(405);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Optional HTTP -> HTTPS redirect for production.
|
||||
if (security_config()['force_https'] && !is_https()) {
|
||||
$host = preg_replace('/[^A-Za-z0-9.\-:\[\]]/', '', (string)($_SERVER['HTTP_HOST'] ?? ''));
|
||||
$uri = (string)($_SERVER['REQUEST_URI'] ?? '/');
|
||||
if ($host !== '' && $uri !== '' && $uri[0] === '/') {
|
||||
header('Location: https://' . $host . $uri, true, 301);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user