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:
+29
-5
@@ -7,17 +7,34 @@
|
||||
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Signed socket token: the Node server joins rooms from these claims only, so a
|
||||
// browser cannot pick another company's room. Must match verifySocketToken() in
|
||||
// nodejs/server.js (key derived from NODE_EMIT_SECRET, HMAC-SHA256, base64url).
|
||||
$_socket_token = '';
|
||||
if (defined('NODE_EMIT_SECRET') && NODE_EMIT_SECRET !== '' && !empty($_SESSION['login_company_id'])) {
|
||||
$_b64url = fn ($s) => rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
|
||||
$_payload = $_b64url(json_encode([
|
||||
'c' => (int)$_SESSION['login_company_id'],
|
||||
'u' => (int)($_SESSION['login_user_id'] ?? 0),
|
||||
'r' => (string)($_SESSION['login_role'] ?? 'viewer'),
|
||||
'exp' => time() + 8 * 3600,
|
||||
]));
|
||||
$_socket_key = hash_hmac('sha256', 'socket-token', NODE_EMIT_SECRET, true);
|
||||
$_socket_token = $_payload . '.' . $_b64url(hash_hmac('sha256', $_payload, $_socket_key, true));
|
||||
}
|
||||
?>
|
||||
<!-- ── Real-time WebSocket connection ──────────────────────────────────────── -->
|
||||
<!-- Socket.io client is served by the Node.js server itself -->
|
||||
<script src="<?php echo NODE_PUBLIC_URL; ?>/socket.io/socket.io.js"></script>
|
||||
<script src="<?php echo htmlspecialchars(NODE_PUBLIC_URL, ENT_QUOTES, 'UTF-8'); ?>/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
// company_id is set in include_topbar.php as a JS global
|
||||
if (typeof company_id === 'undefined' || !company_id) return;
|
||||
if (typeof io === 'undefined') return;
|
||||
|
||||
window._socket = io('<?php echo NODE_PUBLIC_URL; ?>', {
|
||||
query: { company_id: company_id, user_id: user_id, role: user_role },
|
||||
window._socket = io(<?php echo json_encode(NODE_PUBLIC_URL, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>, {
|
||||
auth: { token: <?php echo json_encode($_socket_token, JSON_HEX_TAG); ?> },
|
||||
reconnection: true,
|
||||
reconnectionDelay: 2000,
|
||||
});
|
||||
@@ -57,6 +74,13 @@ function show_toast(title, message, type) {
|
||||
};
|
||||
var icon = icon_map[type] || icon_map.info;
|
||||
|
||||
// Notification text is data, never markup.
|
||||
var esc = function (v) {
|
||||
return String(v).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
};
|
||||
|
||||
var container = document.getElementById('_toast_container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
@@ -77,8 +101,8 @@ function show_toast(title, message, type) {
|
||||
' <div class="toast-body d-flex align-items-start gap-2">',
|
||||
' <i class="ti ' + icon + ' fs-5 mt-1 flex-shrink-0"></i>',
|
||||
' <div>',
|
||||
(title ? '<div class="fw-semibold lh-sm">' + title + '</div>' : ''),
|
||||
(message ? '<div class="small text-muted">' + message + '</div>' : ''),
|
||||
(title ? '<div class="fw-semibold lh-sm">' + esc(title) + '</div>' : ''),
|
||||
(message ? '<div class="small text-muted">' + esc(message) + '</div>' : ''),
|
||||
' </div>',
|
||||
' </div>',
|
||||
' <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>',
|
||||
|
||||
Reference in New Issue
Block a user