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:
@@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
@@ -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']));
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user