Harden web root, secrets and realtime auth

- .htaccess: refuse .git, .env, docker, nodejs, sdlc*, scripts, archives and
  app internals; security headers, HSTS over TLS, optional HTTPS redirect
- uploads served through app/file.php to signed-in users only
- Apache/PHP hardening config for the container (ServerTokens, expose_php)
- least-privilege DB account and APP_SECRET_KEY via docker/php/provision.php;
  SMTP passwords re-encrypted with a random IV (secret_box.php)
- Socket.IO rooms from a PHP-signed token; /emit and cron refuse empty secrets
- escape notification text; CLI guards on build scripts; no fixed demo password
This commit is contained in:
Thanakorn
2026-09-24 14:53:40 +07:00
parent e579dd596c
commit ae98dcdcdd
29 changed files with 525 additions and 42 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
<?php
require_once __DIR__ . '/../module/mailer.php';
require_once __DIR__ . '/../secret_box.php';
class SmtpManager
{
@@ -173,7 +174,7 @@ class SmtpManager
private function encryptPassword(string $plain): string
{
return openssl_encrypt($plain, $this->method, $this->pinkey, 0, $this->iv);
return secret_encrypt($plain, $this->pinkey);
}
}
?>
+3 -2
View File
@@ -11,8 +11,9 @@ header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../config.php';
require_once __DIR__ . '/../../dbconn.php';
$secret = $_SERVER['HTTP_X_CRON_SECRET'] ?? '';
if (!defined('NODE_EMIT_SECRET') || $secret !== NODE_EMIT_SECRET) {
// An empty configured secret must never match an empty header.
$secret = (string)($_SERVER['HTTP_X_CRON_SECRET'] ?? '');
if (!defined('NODE_EMIT_SECRET') || NODE_EMIT_SECRET === '' || !hash_equals((string)NODE_EMIT_SECRET, $secret)) {
http_response_code(403);
exit(json_encode(['success' => 0, 'message' => 'Forbidden']));
}
+2 -1
View File
@@ -72,7 +72,8 @@ class mailer{
"input" => $input
]);
return openssl_decrypt(trim($input["data"]), "AES-256-CBC", $input["key"], 0, "1234567890123456" );
require_once __DIR__ . '/../secret_box.php';
return secret_decrypt((string)$input["data"], (string)$input["key"]);
}
+48
View File
@@ -0,0 +1,48 @@
<?php
/**
* secret_box.php — reversible encryption for stored credentials (SMTP passwords).
*
* Format "v2:<base64(iv . ciphertext)>": AES-256-CBC with a random IV per value and
* a key derived from APP_SECRET_KEY (config.php, from the deployment environment).
*
* Older values were encrypted with the fixed key $pinkey ("wms") and a constant IV,
* which anyone reading the source can undo. secret_decrypt() still reads that legacy
* format so existing rows keep working; setup.php re-encrypts them to v2 and every
* save writes v2. Without APP_SECRET_KEY the legacy format is written (and logged)
* so a deployment that has not set the key yet keeps sending mail.
*/
const SECRET_BOX_LEGACY_IV = '1234567890123456';
function secret_box_key(): ?string {
if (!defined('APP_SECRET_KEY') || APP_SECRET_KEY === '') return null;
return hash('sha256', APP_SECRET_KEY, true);
}
function secret_encrypt(string $plain, string $legacy_key = 'wms'): string {
$key = secret_box_key();
if ($key === null) {
error_log('[secret_box] APP_SECRET_KEY is not set; storing a credential in the legacy format.');
return openssl_encrypt($plain, 'AES-256-CBC', $legacy_key, 0, SECRET_BOX_LEGACY_IV);
}
$iv = random_bytes(16);
$ct = openssl_encrypt($plain, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
return 'v2:' . base64_encode($iv . $ct);
}
/** Returns the plain text, or false when the value cannot be decrypted. */
function secret_decrypt(string $stored, string $legacy_key = 'wms') {
$stored = trim($stored);
if (strncmp($stored, 'v2:', 3) === 0) {
$key = secret_box_key();
$raw = base64_decode(substr($stored, 3), true);
if ($key === null || $raw === false || strlen($raw) <= 16) return false;
return openssl_decrypt(substr($raw, 16), 'AES-256-CBC', $key, OPENSSL_RAW_DATA, substr($raw, 0, 16));
}
return openssl_decrypt($stored, 'AES-256-CBC', $legacy_key, 0, SECRET_BOX_LEGACY_IV);
}
/** Whether a stored value still uses the legacy fixed-key format. */
function secret_is_legacy(string $stored): bool {
return $stored !== '' && strncmp(trim($stored), 'v2:', 3) !== 0;
}