From 0ec3222873e1fd07323464c44dd987e9e70a3035 Mon Sep 17 00:00:00 2001 From: Oleksandr Shuryha Date: Thu, 26 Mar 2026 13:34:33 +0100 Subject: [PATCH] Allow /stats from LAN/private IP ranges --- CHANGELOG.md | 1 + README.md | 2 +- backend/server.js | 39 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f73f8a..00c6c05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - `lastHour`: total visits and unique visitors in the last 60 minutes. - `last5Days`: daily visits and unique visitors for the last 5 UTC calendar days. - Wired visit recording to `GET /api/catalog` so app opens contribute to stats. +- Updated `/stats` access control to allow localhost and LAN/private IP clients, while still blocking public internet IPs. ## 0.1.0 - 2026-03-25 - Created initial frontend-only React + TypeScript project scaffold with Vite. diff --git a/README.md b/README.md index 1a6e19b..36f319d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ A React + TypeScript app with a local Node + SQLite backend for planning what to - Visits grouped by country - Last hour totals (`visits`, `uniqueVisitors`) - Last 5 calendar days (`visits`, `uniqueVisitors` per day) -- `/stats` is localhost-only at backend level (non-local requests receive `403`). +- `/stats` is restricted at backend level to localhost and LAN/private IP ranges only (public requests receive `403`). ## Project Structure - `frontend/`: Vite + React app (`frontend/src`, `frontend/index.html`) diff --git a/backend/server.js b/backend/server.js index c8a8f9b..feb54a9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -236,7 +236,44 @@ function getRequestIp(request) { function isLocalRequest(request) { const ip = getRequestIp(request); - return ip === '127.0.0.1' || ip === '::1' || ip === ''; + if (!ip || ip === '127.0.0.1' || ip === '::1') { + return true; + } + + if (ip.includes(':')) { + const normalized = ip.toLowerCase(); + return ( + normalized.startsWith('fc') || + normalized.startsWith('fd') || + normalized.startsWith('fe80') + ); + } + + const parts = ip.split('.'); + if (parts.length !== 4) { + return false; + } + + const octets = parts.map((part) => Number(part)); + if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) { + return false; + } + + const [a, b] = octets; + if (a === 10) { + return true; + } + if (a === 172 && b >= 16 && b <= 31) { + return true; + } + if (a === 192 && b === 168) { + return true; + } + if (a === 169 && b === 254) { + return true; + } + + return false; } function hashVisitorId(ip, userAgent) {