fix(security): caddy-event self-noise conjunction filter for host curl probes (DC-118) [glm-grade=A]
The host-side uptime watchdog and on-host cron jobs curl Caddy with a stock curl/8.5.0 UA from loopback and the host tailscale IP — ~300 GET /api/health 401 warn-events/day burying real perimeter signal (census: 339+20 of 5000 access.log lines). Dropping on UA alone would blind the store to external curl scanners, so generic tool UAs (curl/) are now dropped ONLY when the source remote_ip is one of this host's own addresses (DASHCADDY_SELF_IPS, default loopback; start.sh derives 127.0.0.1 + tailscale ip -4, empty-safe). remote_ip (TCP peer) is used, never the spoofable client_ip. DashCaddy-* probe UAs stay unconditionally dropped. 9 regression pins cover the full conjunction matrix incl. external-IP+curl KEPT and spoofed-XFF KEPT. 2858/2858 green (128 suites). Judge: glm-4.6@zai-coding-paas cold-read round 1 = A clean (0 blocking), both polish notes folded. URN urn:ump:s2sgitfepze65crtp57dpdw4gk4w7upsoi4tqcvfwahcsicyepba
This commit is contained in:
@@ -240,8 +240,25 @@ function startCaddyWorker({ log: logger = log } = {}) {
|
||||
'DashCaddy-Probe/1.0', // health-checker + upstream watcher
|
||||
'DashCaddy-HealthCheck/1.0', // startup validator
|
||||
];
|
||||
function isSelfNoise(userAgent) {
|
||||
return !!userAgent && SELF_NOISE_UAS.some(ua => userAgent.startsWith(ua));
|
||||
// DC-118: the host-side uptime watchdog and on-host cron jobs curl Caddy
|
||||
// with a stock curl/<ver> UA from the machine's own addresses (~300
|
||||
// events/day of GET /api/health 401). Dropping on UA alone would also
|
||||
// hide a real attacker using curl, so GENERIC UAs are only dropped when
|
||||
// the source IP is one of this host's own addresses: loopback always,
|
||||
// plus DASHCADDY_SELF_IPS (start.sh passes the tailscale IP). Uses
|
||||
// remote_ip (the TCP peer), never client_ip (X-Forwarded-For is
|
||||
// spoofable and must not be able to opt an attacker out of the store).
|
||||
// Prefix match (not equality) so version skew — curl/7.68, curl/8.5,
|
||||
// future curl/10 — all match; curl-impersonate-* deliberately does not.
|
||||
const GENERIC_PROBE_UAS = ['curl/'];
|
||||
const selfIps = new Set(
|
||||
(process.env.DASHCADDY_SELF_IPS || '127.0.0.1,::1')
|
||||
.split(',').map(s => s.trim()).filter(Boolean)
|
||||
);
|
||||
function isSelfNoise(userAgent, ip) {
|
||||
if (!userAgent) return false;
|
||||
if (SELF_NOISE_UAS.some(ua => userAgent.startsWith(ua))) return true;
|
||||
return selfIps.has(ip) && GENERIC_PROBE_UAS.some(ua => userAgent.startsWith(ua));
|
||||
}
|
||||
|
||||
return createTail({
|
||||
@@ -271,7 +288,10 @@ function startCaddyWorker({ log: logger = log } = {}) {
|
||||
// old single-value read always produced null metadata.
|
||||
const uaHeader = (req.headers && (req.headers['User-Agent'] || req.headers['user-agent'])) || null;
|
||||
const userAgent = Array.isArray(uaHeader) ? uaHeader[0] : uaHeader;
|
||||
if (isSelfNoise(userAgent)) return;
|
||||
// DC-118: conjunction filter — see isSelfNoise. remote_ip (TCP peer),
|
||||
// never client_ip (spoofable X-Forwarded-For must not opt an attacker
|
||||
// out of the security store).
|
||||
if (isSelfNoise(userAgent, ip)) return;
|
||||
|
||||
// Severity mapping
|
||||
let severity = 'info';
|
||||
|
||||
Reference in New Issue
Block a user