mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-18 09:04:54 +00:00
* docs: PR2 GDPR IP/privacy audit design spec Second of five GDPR PRs (#6701). Audit identifies four log-sites that leak IPs despite disableIPlogging=true, proposes a tri-state ipLogging setting with a back-compat shim, and specifies a doc/privacy.md that documents Etherpad's actual IP handling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: PR2 GDPR IP/privacy audit implementation plan 7 TDD-structured tasks: anonymizeIp helper + unit tests, tri-state ipLogging setting with disableIPlogging deprecation shim, wiring through 5 leaking log sites, clientVars.clientIp removal, access-log integration test, doc/privacy.md, and PR handoff. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(gdpr): anonymizeIp helper with v4/v6/v4-mapped truncation * feat(gdpr): tri-state ipLogging setting + disableIPlogging shim * fix(gdpr): route every IP log site through anonymizeIp Closes four leaks where disableIPlogging was silently ignored (rate-limit warn, both auth-log calls in webaccess, import/export rate-limit warn) and normalises the four that did honour the flag onto the new ipLogging tri-state via the shared helper. * chore(gdpr): drop dead clientVars.clientIp placeholder Server side: remove the literal '127.0.0.1' assignments from both clientVars and collab_client_vars. Type side: drop clientIp from ClientVarPayload and ServerVar. pad.getClientIp now returns the same '127.0.0.1' literal as a plugin-compat shim (pad_utils.uniqueId still uses it as a prefix). * test(gdpr): ipLogging modes + disableIPlogging shim * docs(gdpr): operator-facing privacy and IP handling statement * fix(gdpr): validate ipLogging at load + regression test for log sites Qodo review: - settings.ipLogging is loaded as a trusted union but nothing enforced the shape. An unknown value (e.g. a typo or null) silently fell through to anonymizeIp's "truncated" branch and emitted partially redacted IPs. Fall back to "anonymous" with a WARN at load time. - New regression test scans the four known log-sites for raw req.ip / socket.request.ip / request.ip inside logger calls that don't wrap through anonymizeIp / logIp, so a future edit that re-introduces a raw IP fails CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
35 lines
1.4 KiB
TypeScript
35 lines
1.4 KiB
TypeScript
'use strict';
|
|
|
|
import {isIP} from 'node:net';
|
|
|
|
export type IpLogging = 'full' | 'truncated' | 'anonymous';
|
|
|
|
const IPV4_MAPPED = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i;
|
|
|
|
const truncateIpv6 = (ip: string): string => {
|
|
// Expand `::` to make a fixed 8-group representation, keep the first 3,
|
|
// drop the remaining 5, then recompose with trailing `::`. Collapse trailing
|
|
// zero groups in the kept prefix so `2001:db8:0::` becomes `2001:db8::`.
|
|
const [head, tail] = ip.split('::');
|
|
const headParts = head === '' ? [] : head.split(':');
|
|
const tailParts = tail == null ? [] : tail === '' ? [] : tail.split(':');
|
|
const missing = 8 - headParts.length - tailParts.length;
|
|
const full = [...headParts, ...Array(Math.max(0, missing)).fill('0'), ...tailParts];
|
|
const keep = full.slice(0, 3).map((g) => g.toLowerCase().replace(/^0+(?=.)/, ''));
|
|
while (keep.length > 0 && keep[keep.length - 1] === '0') keep.pop();
|
|
return `${keep.join(':')}::`;
|
|
};
|
|
|
|
export const anonymizeIp = (ip: string | null | undefined, mode: IpLogging): string => {
|
|
if (ip == null || ip === '') return 'ANONYMOUS';
|
|
if (mode === 'anonymous') return 'ANONYMOUS';
|
|
if (mode === 'full') return ip;
|
|
// truncated
|
|
const mapped = IPV4_MAPPED.exec(ip);
|
|
if (mapped != null) return `::ffff:${mapped[1].replace(/\.\d+$/, '.0')}`;
|
|
switch (isIP(ip)) {
|
|
case 4: return ip.replace(/\.\d+$/, '.0');
|
|
case 6: return truncateIpv6(ip);
|
|
default: return 'ANONYMOUS';
|
|
}
|
|
};
|