Skip to content

Commit 0db3ef0

Browse files
Apolloccryptclaude
andcommitted
security: fix all findings from 2026-04-10 and 2026-04-11 audit reports
## 2026-04-11 report Fix 1 HIGH admin/server.js: add per-IP rate limiting on POST /auth/login (5 attempts / 15 min, proxy-aware X-Forwarded-For) Fix 2 MED relay.js: replace 3 remaining plain === ADMIN_TOKEN comparisons with safeEqual() (/health, /metrics, /v2/reload-users) Fix 3 MED admin/server.js: remove pgp_ enterprise path — require ADMIN_TOKEN only Fix 4 MED relay.js /v2/dl/:token/get: defer blob deletion to res 'finish' event; block concurrent downloads with in_progress flag; clear flag on socket close before finish (allow retry) Fix 5 MED relay.js verifyTotp: evaluate all windows without early return; use timingSafeEqual; reject reused codes via _usedTotpCodes map Fix 6 MED relay.js: replace readFileSync/writeFileSync in key create/revoke with serialized async write queue (_writeUsersJson) Fix 7 MED relay.js: paginate GET /v2/relays (limit/offset, max 200); evict oldest registry entry when MAX_RELAY_REGISTRY reached Fix 8 MED relay.js: replace appendFileSync CT log writes with async write stream; log rotation when file exceeds CT_MAX_SIZE (default 100 MB); flush queue on SIGTERM/SIGINT Fix 9 LOW relay.js: replace O(n) DID registry scans with O(1) didRegistry.get() Fix 10 LOW admin/server.js: strip internal error detail from 502 response Fix 11 MED relay.js isSsrfSafeUrl(): restrict webhook ports to 443 (HTTPS only) Fix 12 LOW relay.js: close active WebSocket connections on key revocation Fix 13 LOW relay.js: validate plan string against VALID_PLANS allowlist Fix 14 LOW relay.js base32Decode: throw on invalid Base32 char; validate TOTP_SECRET at startup ## 2026-04-10 report Fix A HIGH fly-relay not present in this repo — N/A Fix B HIGH relay.js: stream-next now returns real blob hashes from per-device delivery queue (deviceQueues) populated on /v2/inbound Fix C HIGH relay.js: DNS rebinding check already present (pushWebhooks) — verified Fix D MED sdk-js/index.js: detectRelay sends API key in X-Api-Key header instead of ?k= query param; fail explicitly on no relay found Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e32186e commit 0db3ef0

3 files changed

Lines changed: 237 additions & 58 deletions

File tree

admin/server.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ if (!ADMIN_TOKEN) { console.error('[PARAMANT-ADMIN] ADMIN_TOKEN is not set — r
2020
const sessions = new Map();
2121
setInterval(() => { const now = Date.now(); for (const [k, v] of sessions) if (v.expires < now) sessions.delete(k); }, 60_000);
2222

23+
// Fix 1: rate limiting on /auth/login — 5 attempts per 15 min per IP
24+
const loginAttempts = new Map(); // ip → { count, resetAt }
25+
function checkLoginRateLimit(ip) {
26+
const now = Date.now();
27+
const b = loginAttempts.get(ip) || { count: 0, resetAt: now + 15 * 60_000 };
28+
if (now > b.resetAt) { b.count = 0; b.resetAt = now + 15 * 60_000; }
29+
if (b.count >= 5) return false;
30+
b.count++;
31+
loginAttempts.set(ip, b);
32+
return true;
33+
}
34+
setInterval(() => { const now = Date.now(); for (const [k, v] of loginAttempts) if (now > v.resetAt + 60_000) loginAttempts.delete(k); }, 120_000);
35+
2336
function authMiddleware(req, res, next) {
2437
const s = sessions.get((req.headers['x-session'] || '').trim());
2538
if (!s || s.expires < Date.now()) return res.status(401).json({ error: 'unauthorized' });
@@ -78,22 +91,33 @@ app.use(BASE_PATH || '/', express.static(path.join(__dirname, 'public')));
7891
const api = express.Router();
7992

8093
api.post('/auth/login', async (req, res) => {
94+
// Fix 1: rate limit by IP (proxy-aware — trust X-Forwarded-For behind nginx)
95+
const ip = (req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown').split(',')[0].trim();
96+
if (!checkLoginRateLimit(ip)) {
97+
return res.status(429).json({ error: 'Too many login attempts — try again in 15 minutes' });
98+
}
8199
const { token, totp } = req.body || {};
82100
if (!token) return res.status(401).json({ error: 'Token required' });
83101
if (!totp || !/^\d{6}$/.test(totp)) return res.status(400).json({ error: 'TOTP code required (6 digits)' });
84-
// M1: timing-safe comparison prevents timing-oracle attacks on the token
102+
// Fix 1 + Fix 3: timing-safe ADMIN_TOKEN comparison only — pgp_ enterprise path removed
103+
// (pgp_ keys are regular API keys managed per-sector; they don't grant admin access)
104+
const tokenBuf = Buffer.from(token, 'utf8');
105+
const adminBuf = Buffer.from(ADMIN_TOKEN, 'utf8');
85106
const isMaster = ADMIN_TOKEN.length > 0
86-
&& token.length === ADMIN_TOKEN.length
87-
&& crypto.timingSafeEqual(Buffer.from(token), Buffer.from(ADMIN_TOKEN));
88-
const isEnterprise = token.startsWith('pgp_');
89-
if (!isMaster && !isEnterprise) return res.status(401).json({ error: 'Invalid token — use your ADMIN_TOKEN or an enterprise pgp_ key' });
107+
&& tokenBuf.length === adminBuf.length
108+
&& crypto.timingSafeEqual(tokenBuf, adminBuf);
109+
if (!isMaster) return res.status(401).json({ error: 'Invalid token' });
90110
try {
91-
const r = await relayFetch('health', '/v2/admin/verify-mfa', 'POST', { totp_code: totp }, false, token);
111+
const r = await relayFetch('health', '/v2/admin/verify-mfa', 'POST', { totp_code: totp }, false, ADMIN_TOKEN);
92112
if (!r.body?.ok) return res.status(401).json({ error: 'Invalid TOTP code' });
93113
const sid = crypto.randomBytes(32).toString('hex');
94-
sessions.set(sid, { expires: Date.now() + 3_600_000, token });
114+
sessions.set(sid, { expires: Date.now() + 3_600_000, token: ADMIN_TOKEN });
95115
return res.json({ ok: true, session: sid, expires_in: 3600 });
96-
} catch (e) { return res.status(502).json({ error: `Relay unreachable: ${e.message}` }); }
116+
} catch (e) {
117+
// Fix 10: don't leak internal relay address in error response
118+
console.error('[admin] relay unreachable:', e.message);
119+
return res.status(502).json({ error: 'Relay unreachable' });
120+
}
97121
});
98122

99123
api.post('/auth/logout', (req, res) => { sessions.delete((req.headers['x-session'] || '').trim()); res.json({ ok: true }); });

0 commit comments

Comments
 (0)