POSA_LEAKSMS/tests/ingest_reject_log_test.php
cw2007 7fa39406d3 feat(observability): 수집 거부 출처 IP 로그 + 대시보드 노출
수집 거부 카운터는 사유별 숫자만 남겨, "우리 노드 오설정 vs 외부 스캐너"를
구분할 수 없었다. 서명불일치 2건이 우리 노드의 키 불일치인지 인터넷 프로브인지
대시보드만으론 판별 불가했다.

- config.php: 최근 거부 이벤트 링버퍼(var/ingest_reject_events.json, 상한
  INGEST_REJECT_EVENTS_MAX=50, flock 기반 DoS 안전) 추가. record_ingest_rejection()
  이 모든 거부에서 출처 IP(REMOTE_ADDR, 형식검증)를 자동 기록한다. 본문 유래
  값(device_id 등)은 신뢰불가 → ingest_sanitize_token() 으로 제어문자 제거·절단
  (로그 인젝션 방어). var/ 는 .htaccess 로 웹 접근 차단됨.
- api/sensor_data.php: 서명 거부 시 서명모드(raw=키불일치/none=헤더스트립)와
  device_id 를 컨텍스트로 함께 남겨 출처 판별을 돕는다.
- dashboard.php: '수집 거부' 위젯 아래 '최근 거부 출처' 표(시각·IP·사유·모드·
  device_id) 노출. 모든 필드 htmlspecialchars.
- tests/ingest_reject_log_test.php: 로그 인젝션 제거·IP 형식검증 회귀 추가,
  run_tests.sh 등록. security_hardening_test.php 소스 회귀 5종 추가.

주: config.php 에는 작업 트리에 있던 선행 변경(METRIC_SENSOR_IDS — 대시보드
온습도 센서카드 목록)이 함께 포함된다.
2026-07-09 13:43:12 +09:00

55 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
// 수집 거부 출처 로그 헬퍼 회귀 (DB/파일 없이 순수 로직만).
// ingest_sanitize_token(): 로그 인젝션 방지(제어문자 제거·절단)
// ingest_client_ip(): REMOTE_ADDR 형식검증(부재/위조형식 → '?')
// 함수/상수 존재: record_ingest_reject_event / read_ingest_reject_events / INGEST_REJECT_EVENTS_MAX
$root = dirname(__DIR__);
require_once $root . '/php/config.php';
$failures = [];
function check(bool $cond, string $msg): void { global $failures; if (!$cond) { $failures[] = $msg; } }
function eq($a, $b, string $msg): void {
check($a === $b, $msg . ' (expected ' . var_export($b, true) . ', got ' . var_export($a, true) . ')');
}
// ── 함수/상수 존재 ──────────────────────────────────────────
check(function_exists('ingest_sanitize_token'), 'ingest_sanitize_token() should exist.');
check(function_exists('ingest_client_ip'), 'ingest_client_ip() should exist.');
check(function_exists('record_ingest_reject_event'), 'record_ingest_reject_event() should exist.');
check(function_exists('read_ingest_reject_events'), 'read_ingest_reject_events() should exist.');
check(defined('INGEST_REJECT_EVENTS_MAX'), 'INGEST_REJECT_EVENTS_MAX should be defined.');
// ── ingest_sanitize_token: 로그 인젝션/제어문자 방지 ────────
eq(ingest_sanitize_token('rpi4-leak-02', 32), 'rpi4-leak-02', 'plain token unchanged');
// 개행/캐리지리턴 → 위조 로그줄 주입 차단(제어문자는 공백 처리)
check(strpos(ingest_sanitize_token("rpi\n[API] 서명 불일치 from evil", 64), "\n") === false,
'newline stripped (log injection blocked)');
check(strpos(ingest_sanitize_token("a\r\nb", 64), "\r") === false, 'carriage return stripped');
check(strpos(ingest_sanitize_token("a\tb", 64), "\t") === false, 'tab stripped');
// 길이 절단
check(mb_strlen(ingest_sanitize_token(str_repeat('x', 100), 32)) === 32, 'truncated to max length');
eq(ingest_sanitize_token(' spaced ', 32), 'spaced', 'trimmed');
eq(ingest_sanitize_token('abc', 0), 'abc', 'max 0 -> no truncation');
// ── ingest_client_ip: REMOTE_ADDR 형식검증 ──────────────────
$_SERVER['REMOTE_ADDR'] = '203.0.113.9';
eq(ingest_client_ip(), '203.0.113.9', 'valid IPv4 returned');
$_SERVER['REMOTE_ADDR'] = '2001:db8::1';
eq(ingest_client_ip(), '2001:db8::1', 'valid IPv6 returned');
$_SERVER['REMOTE_ADDR'] = 'not-an-ip';
eq(ingest_client_ip(), '?', 'invalid IP format -> ?');
unset($_SERVER['REMOTE_ADDR']);
eq(ingest_client_ip(), '?', 'missing REMOTE_ADDR -> ?');
if ($failures) {
fwrite(STDERR, "Ingest reject-log test failures:\n");
foreach ($failures as $f) {
fwrite(STDERR, "- {$f}\n");
}
exit(1);
}
echo "Ingest reject-log checks passed.\n";