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
+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;
}