Analyse: Messdienst: Ping ohne Einwilligung, Zeitstempel auf Stunde, Pfade ohne Kennungen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 18:50:26 +00:00
1 parent 8d64a18d35
commit 96409c6306
7 files changed
+149 -10

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ export function verifyHeatToken(token, secret, nowMs, { ignoreExpiry = false } =
export function createHeat({ db, config = {}, now }) {
const clock = now || Date.now;
const getSite = db.prepare('SELECT domains, active FROM sites WHERE siteKey = ?');
const getClicks = db.prepare('SELECT x, y FROM clicks WHERE site = ? AND path = ? AND vp = ? AND ts >= ? ORDER BY ts DESC LIMIT ?');
const getClicks = db.prepare('SELECT x, y FROM clicks WHERE site = ? AND path = ? AND vp = ? AND ts >= ? ORDER BY ts DESC, id DESC LIMIT ?');
// CORS: nur die eigenen https-Domains der Website (das Overlay läuft auf der Kundenseite).
function cors(res, req, domains) {
+22 -6
View File
@@ -10,6 +10,7 @@ const HOUR = 3600 * 1000;
const MINUTE = 60 * 1000;
const FIELDS = Object.assign(Object.create(null), {
pv: ['t', 'site', 'path', 'ref', 'vp'],
cx: ['t', 'site', 'path', 'ref', 'vp'],
bn: ['t', 'site', 'a'],
en: ['t', 'site', 'path', 'vp', 'sec', 'scroll'],
ck: ['t', 'site', 'path', 'vp', 'x', 'y', 'sel'],
@@ -55,11 +56,19 @@ function anyHostOf(value) {
try { return new URL(value).hostname.toLowerCase(); } catch { return ''; }
}
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
// Kennungsartige Segmente (lange Ziffernfolge, UUID, E-Mail) werden nie gespeichert.
function looksLikeId(seg) {
let d = seg;
try { d = decodeURIComponent(seg); } catch { /* Rohwert */ }
return /\d{6,}/.test(d) || UUID.test(d) || d.includes('@');
}
function cleanPath(p) {
if (typeof p !== 'string') return null;
const s = p.split(/[?#]/)[0];
if (!s.startsWith('/') || s.length > 300 || /[\u0000-\u001f\u007f]/.test(s)) return null;
return s;
return s.split('/').map((seg) => (looksLikeId(seg) ? ':id' : seg)).join('/');
}
function cleanHost(h) {
@@ -134,12 +143,19 @@ export function createIngest({ db, geo = { lookup: () => '' }, salt, now = Date.
}
const path = cleanPath(e.path);
if (!path) return;
if (e.t === 'pv') {
const hour = Math.floor(t / HOUR) * HOUR;
if (e.t === 'cx') {
const vp = cleanVp(e.vp);
if (!vp) return;
insEvent.run(site, hour, 'cx', path, vp, cleanRef(e.ref, domains), null, null, null, null, null, null, null);
return;
}
if (e.t === 'pv') {
const vp = e.vp === undefined ? null : cleanVp(e.vp);
if (e.vp !== undefined && !vp) return;
const c = String(geo.lookup(ip) || '');
const country = /^[A-Za-z]{2}$/.test(c) ? c.toUpperCase() : '';
insEvent.run(site, t, 'pv', path, vp, cleanRef(e.ref, domains), country, classifyUa(ua).device, null, null, null, null, null);
insEvent.run(site, hour, 'pv', path, vp, vp ? cleanRef(e.ref, domains) : null, country, classifyUa(ua).device, null, null, null, null, null);
return;
}
if (e.t === 'ck') {
@@ -148,7 +164,7 @@ export function createIngest({ db, geo = { lookup: () => '' }, salt, now = Date.
const y = num(e.y);
if (!vp || x === null || y === null) return;
const sel = typeof e.sel === 'string' ? e.sel.slice(0, 200) : null;
insClick.run(site, t, path, vp, Math.round(clamp(x, 0, 1) * 1000) / 1000, Math.round(clamp(y, 0, 1) * 1000) / 1000, sel);
insClick.run(site, hour, path, vp, Math.round(clamp(x, 0, 1) * 1000) / 1000, Math.round(clamp(y, 0, 1) * 1000) / 1000, sel);
return;
}
const visitor = salt.visitorId(site, ip, ua);
@@ -157,14 +173,14 @@ export function createIngest({ db, geo = { lookup: () => '' }, salt, now = Date.
const sec = num(e.sec);
const scroll = num(e.scroll);
if (!vp || sec === null || scroll === null) return;
insEvent.run(site, t, 'en', path, vp, null, null, null, visitor, Math.floor(clamp(sec, 0, 3600)), Math.floor(clamp(scroll, 0, 100)), null, null);
insEvent.run(site, hour, 'en', path, vp, null, null, null, visitor, Math.floor(clamp(sec, 0, 3600)), Math.floor(clamp(scroll, 0, 100)), null, null);
return;
}
if (e.t === 'ct') {
if (!['tel', 'mail', 'wa', 'out'].includes(e.kind)) return;
let host = null;
if (e.kind === 'out') { host = cleanHost(e.host); if (!host) return; }
insEvent.run(site, t, 'ct', path, null, null, null, null, visitor, null, null, e.kind, host);
insEvent.run(site, hour, 'ct', path, null, null, null, null, visitor, null, null, e.kind, host);
}
}
+5 -2
View File
@@ -19,8 +19,11 @@ export function aggregateRows(rows) {
const visitors = new Set();
for (const r of rows) {
if (r.type === 'pv') {
add('pv_total', ''); add('pv_path', r.path || ''); add('pv_ref', r.ref || '');
add('pv_total', ''); add('pv_path', r.path || '');
if (r.vp != null) add('pv_ref', r.ref || ''); // nur Besucher mit Einwilligung; sonst kommt pv_ref aus cx
add('pv_country', r.country || ''); add('pv_device', r.device || '');
} else if (r.type === 'cx') {
add('pv_ref', r.ref || '');
} else if (r.type === 'en') {
add('en_count', ''); add('en_sec_sum', '', r.sec || 0); add('scroll_sum', '', r.scroll || 0);
if (r.visitor) visitors.add(r.visitor);
@@ -40,7 +43,7 @@ export function runNightly(db, { now = Date.now() } = {}) {
const rawCut = now - RAW_DAYS * DAY_MS;
const offCut = new Date(now - OFF_DAYS * DAY_MS).toISOString();
const siteKeys = db.prepare(`SELECT DISTINCT site FROM events_raw UNION SELECT siteKey FROM sites`).all().map((r) => r.site);
const selRaw = db.prepare('SELECT ts, type, path, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts < ? ORDER BY ts');
const selRaw = db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts < ? ORDER BY ts');
const selDone = db.prepare('SELECT day FROM agg_state WHERE site = ?');
const insDaily = db.prepare(`INSERT INTO daily (site, day, dim, key, count) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(site, day, dim, key) DO UPDATE SET count = count + excluded.count`);
+1 -1
View File
@@ -37,7 +37,7 @@ export function computeStats(db, site, range, now = Date.now()) {
// Tage ohne agg_state (heute, ggf. gestern vor 02:30) live aus den Rohdaten
const live = new Map();
const from = now - (range + 1) * DAY_MS;
for (const r of db.prepare('SELECT ts, type, path, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts >= ? AND ts <= ? ORDER BY ts')
for (const r of db.prepare('SELECT ts, type, path, vp, ref, country, device, visitor, sec, scroll, kind FROM events_raw WHERE site = ? AND ts >= ? AND ts <= ? ORDER BY ts')
.all(site, from, now)) {
const d = dayOf(r.ts);
if (!inRange.has(d) || done.has(d)) continue;