SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트). 전체 코드리뷰에서 도출된 보안 하드닝 10건 반영: - 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시) - 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php) - 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점) - CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php) - 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화 _backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
115 lines
7.3 KiB
PHP
115 lines
7.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
$root = dirname(__DIR__);
|
|
$failures = [];
|
|
|
|
function check(bool $condition, string $message): void {
|
|
global $failures;
|
|
if (!$condition) {
|
|
$failures[] = $message;
|
|
}
|
|
}
|
|
|
|
function file_text(string $path): string {
|
|
return is_file($path) ? (string)file_get_contents($path) : '';
|
|
}
|
|
|
|
$adminSecurity = $root . '/php/admin_security.php';
|
|
check(is_file($adminSecurity), 'php/admin_security.php should provide shared MFA/audit helpers.');
|
|
|
|
if (is_file($adminSecurity)) {
|
|
require_once $adminSecurity;
|
|
check(function_exists('admin_totp_code'), 'admin_totp_code() should exist.');
|
|
check(function_exists('admin_totp_verify'), 'admin_totp_verify() should exist.');
|
|
check(function_exists('admin_totp_secret'), 'admin_totp_secret() should exist.');
|
|
check(function_exists('admin_totp_uri'), 'admin_totp_uri() should exist.');
|
|
check(function_exists('admin_audit_log'), 'admin_audit_log() should exist.');
|
|
|
|
if (function_exists('admin_totp_code') && function_exists('admin_totp_verify') && function_exists('admin_totp_uri')) {
|
|
$secret = 'JBSWY3DPEHPK3PXP';
|
|
$time = 1716200000;
|
|
$code = admin_totp_code($secret, $time);
|
|
check((bool)preg_match('/^\d{6}$/', $code), 'admin_totp_code() should return a six digit code.');
|
|
check(admin_totp_verify($secret, $code, $time, 0), 'admin_totp_verify() should accept the current timestep code.');
|
|
check(!admin_totp_verify($secret, '000000', $time, 0), 'admin_totp_verify() should reject an invalid code.');
|
|
check(admin_totp_code('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', 59) === '287082', 'admin_totp_code() should match the RFC 6238 SHA1 vector truncated to six digits.');
|
|
$uri = admin_totp_uri('SHT30 SMS', 'admin', $secret);
|
|
check(str_starts_with($uri, 'otpauth://totp/'), 'admin_totp_uri() should generate an otpauth URI.');
|
|
check(str_contains($uri, 'issuer=SHT30%20SMS'), 'admin_totp_uri() should include the issuer.');
|
|
check(str_contains($uri, 'secret=' . $secret), 'admin_totp_uri() should include the secret.');
|
|
}
|
|
|
|
if (function_exists('admin_totp_secret')) {
|
|
$generatedSecret = admin_totp_secret();
|
|
check((bool)preg_match('/^[A-Z2-7]{32}$/', $generatedSecret), 'admin_totp_secret() should generate a 32 character Base32 secret.');
|
|
}
|
|
}
|
|
|
|
$config = file_text($root . '/php/config.php');
|
|
$configExample = file_text($root . '/php/config.local.example.php');
|
|
check(str_contains($config, 'ADMIN_TOTP_SECRET'), 'config.php should define ADMIN_TOTP_SECRET.');
|
|
check(str_contains($configExample, 'ADMIN_TOTP_SECRET'), 'config.local.example.php should document ADMIN_TOTP_SECRET.');
|
|
check(str_contains($config, 'MFA_SETUP_TOKEN'), 'config.php should define MFA_SETUP_TOKEN.');
|
|
check(str_contains($configExample, 'MFA_SETUP_TOKEN'), 'config.local.example.php should document MFA_SETUP_TOKEN.');
|
|
|
|
$login = file_text($root . '/php/login.php');
|
|
check(str_contains($login, 'admin_totp_verify'), 'login.php should verify MFA codes.');
|
|
check(str_contains($login, 'otp_code'), 'login.php should render an OTP input.');
|
|
check(str_contains($login, 'admin_audit_log'), 'login.php should write administrator audit events.');
|
|
|
|
$setupMfa = $root . '/php/setup_mfa.php';
|
|
check(is_file($setupMfa), 'php/setup_mfa.php should provide Google Authenticator enrollment.');
|
|
$setupMfaText = file_text($setupMfa);
|
|
check(str_contains($setupMfaText, 'otpauth://'), 'setup_mfa.php should expose an otpauth enrollment URI.');
|
|
check(str_contains($setupMfaText, 'Google Authenticator'), 'setup_mfa.php should mention Google Authenticator enrollment.');
|
|
check(str_contains($setupMfaText, 'MFA_SETUP_TOKEN'), 'setup_mfa.php should require a bootstrap setup token when not logged in.');
|
|
check(str_contains($setupMfaText, 'admin_totp_verify'), 'setup_mfa.php should verify a test code before showing config output.');
|
|
|
|
// ── 온습도 임계 경보 / SMS 기능 회귀 ────────────────────────
|
|
$smsSend = file_text($root . '/php/sms_send.php');
|
|
check(str_contains($config, 'METRIC_TEMP_HIGH_C'), 'config.php should define the METRIC_TEMP_HIGH_C threshold.');
|
|
check(str_contains($config, 'METRIC_RH_HIGH'), 'config.php should define the METRIC_RH_HIGH threshold.');
|
|
check(str_contains($config, 'METRIC_ALERT_COOLDOWN_SEC'), 'config.php should define METRIC_ALERT_COOLDOWN_SEC.');
|
|
check(str_contains($smsSend, 'function send_metric_alert_sms'), 'sms_send.php should provide send_metric_alert_sms().');
|
|
check(str_contains($smsSend, 'function send_metric_recovery_sms'), 'sms_send.php should provide send_metric_recovery_sms().');
|
|
check(str_contains($smsSend, 'function evaluate_metric_thresholds'), 'sms_send.php should provide evaluate_metric_thresholds().');
|
|
|
|
// ── 제거된 누수/사진/사고 기능이 재유입되지 않았는지 ────────
|
|
$dashboard = file_text($root . '/php/dashboard.php');
|
|
$monthly = file_text($root . '/php/monthly_report.php');
|
|
check(!str_contains($dashboard, 'leak_photo') && !str_contains($dashboard, 'admin_photo_url'), 'dashboard.php should not reference removed photo/leak features.');
|
|
check(!str_contains($monthly, 'leak_incident') && !str_contains($monthly, 'admin_photo_url'), 'monthly_report.php should not reference removed photo/incident features.');
|
|
check(!is_file($root . '/php/photo.php'), 'php/photo.php (leak photo proxy) should be removed.');
|
|
check(!is_file($root . '/php/incidents.php'), 'php/incidents.php should be removed.');
|
|
check(!is_file($root . '/php/api/photo_upload.php'), 'php/api/photo_upload.php should be removed.');
|
|
|
|
$retention = $root . '/php/retention_cleanup.php';
|
|
check(is_file($retention), 'php/retention_cleanup.php should implement retention cleanup.');
|
|
$retentionText = file_text($retention);
|
|
check(str_contains($retentionText, 'SENSOR_METRIC_RETENTION_DAYS'), 'retention cleanup should use SENSOR_METRIC_RETENTION_DAYS.');
|
|
check(str_contains($retentionText, 'SMS_LOG_RETENTION_DAYS'), 'retention cleanup should use SMS_LOG_RETENTION_DAYS.');
|
|
check(str_contains($retentionText, 'SENSOR_LOG_RETENTION_DAYS'), 'retention cleanup should use SENSOR_LOG_RETENTION_DAYS.');
|
|
check(str_contains($retentionText, '--dry-run'), 'retention cleanup should support --dry-run.');
|
|
|
|
$backup = $root . '/scripts/backup_evidence.php';
|
|
check(is_file($backup), 'scripts/backup_evidence.php should generate backup/restore evidence.');
|
|
$backupText = file_text($backup);
|
|
check(str_contains($backupText, 'backup-evidence'), 'backup evidence script should generate a backup evidence report.');
|
|
check(str_contains($backupText, 'restore'), 'backup evidence script should include restore test evidence fields.');
|
|
|
|
$securityEvidence = file_text($root . '/php/security_evidence.php');
|
|
check(str_contains($securityEvidence, '관리자 MFA'), 'security_evidence.php should report administrator MFA status.');
|
|
check(str_contains($securityEvidence, '감사로그'), 'security_evidence.php should report audit log status.');
|
|
check(str_contains($securityEvidence, '보관기간'), 'security_evidence.php should report retention controls.');
|
|
check(str_contains($securityEvidence, '백업'), 'security_evidence.php should report backup evidence controls.');
|
|
|
|
if ($failures) {
|
|
fwrite(STDERR, "Security hardening regression failures:\n");
|
|
foreach ($failures as $failure) {
|
|
fwrite(STDERR, "- {$failure}\n");
|
|
}
|
|
exit(1);
|
|
}
|
|
|
|
echo "Security hardening regression checks passed.\n";
|