fix: full IPs, top attacked form banner, Bearer token auth on /submit

server.js:
- Remove maskIP() — store full IPs as submitted (sanitizeIP trims/truncates only)
- Add requireToken() middleware with constant-time comparison (timingSafeEqual)
  using 128-byte padded buffers to prevent length-based timing leaks
- API_TOKEN env var — if unset the endpoint stays open (dev mode); set it in prod
- /api/v1/submit now requires Authorization: Bearer <token>

docker-compose.yml / .env.example:
- Expose API_TOKEN env var with clear comment

index.html:
- Add red-bordered 'MOST ATTACKED FORM (30D)' banner between stats and content grid
  showing form name, hit count, and % of all 30d blocks
- Widen live feed IP column 90px → 130px to fit full IPv4 addresses
- Remove 'ALL DATA IS ANONYMISED' from footer (IPs are full now)

honeypot-fields.php:
- SmartHoneypotAPIClient: add api_token to defaults + send Authorization header
- save_api_settings: persist api_token field
- Settings tab: add password input for API token with description
This commit is contained in:
2026-03-09 19:26:23 +01:00
parent 6740180981
commit bd5a67b57f
5 changed files with 105 additions and 19 deletions

View File

@@ -211,7 +211,7 @@ main { padding: 14px 16px; max-width: 1700px; margin: 0 auto; }
.feed-row {
display: grid;
grid-template-columns: 62px 90px auto;
grid-template-columns: 62px 130px auto;
gap: 6px;
border-bottom: 1px solid var(--muted);
padding: 1px 0;
@@ -272,6 +272,39 @@ footer {
gap: 6px;
}
/* ── Top target banner ──────────────────────────────────────────────── */
#top-target {
background: var(--bg2);
border: 1px solid #2a0000;
border-left: 3px solid var(--red);
padding: 10px 16px;
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
#top-target .tt-label {
font-size: 10px;
letter-spacing: 3px;
color: var(--dim);
}
#top-target .tt-form {
font-size: 15px;
font-weight: bold;
color: var(--red);
text-shadow: 0 0 10px var(--red);
letter-spacing: 1px;
}
#top-target .tt-hits {
font-size: 11px;
color: var(--amber);
}
#top-target .tt-pct {
font-size: 11px;
color: var(--dim);
}
/* ── Responsive ─────────────────────────────────────────────────────── */
@media (max-width: 1100px) {
.stats-row { grid-template-columns: repeat(3, 1fr); }
@@ -321,6 +354,14 @@ footer {
</div>
</div>
<!-- ── Most attacked form ────────────────────────────────────────── -->
<div id="top-target">
<span class="tt-label">▶ MOST ATTACKED FORM (30D):</span>
<span class="tt-form" id="tt-form"></span>
<span class="tt-hits" id="tt-hits"></span>
<span class="tt-pct" id="tt-pct"></span>
</div>
<!-- ── Content grid ───────────────────────────────────────────────── -->
<div class="content-grid">
@@ -402,7 +443,7 @@ footer {
</main>
<footer>
<span>HONEYPOT NETWORK MONITOR // ANONYMOUS THREAT INTELLIGENCE // ALL DATA IS ANONYMISED</span>
<span>HONEYPOT NETWORK MONITOR // CENTRALIZED THREAT INTELLIGENCE</span>
<span>REFRESHED: <span id="last-update">--</span></span>
</footer>
@@ -602,6 +643,15 @@ async function fetchStats() {
renderBars(document.getElementById('bars-reasons'), s.top_reasons);
renderAttackers(s.top_ips);
// Top attacked form banner
if (s.top_forms && s.top_forms.length) {
const top = s.top_forms[0];
document.getElementById('tt-form').textContent = top.form_type;
document.getElementById('tt-hits').textContent = `${top.hits.toLocaleString()} hits`;
const pct = s.last_30d > 0 ? Math.round(top.hits / s.last_30d * 100) : 0;
document.getElementById('tt-pct').textContent = `(${pct}% of all blocks)`;
}
window._hourly = s.hourly;
drawChart(s.hourly);