-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecurity.js
More file actions
129 lines (117 loc) · 4.85 KB
/
Copy pathsecurity.js
File metadata and controls
129 lines (117 loc) · 4.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// Security utilities — timing-safe HMAC, admin auth, request validation
const crypto = require('crypto');
/**
* Timing-safe HMAC SHA-256 verifikácia.
* @param {Buffer|string} rawBody - raw request body
* @param {string} secret - HMAC kľúč
* @param {string} signatureHeader - hodnota zo `BTCPay-Sig` headera (formát: "sha256=hex...")
* @returns {boolean}
*/
function verifyHmacSignature(rawBody, secret, signatureHeader) {
if (!signatureHeader || typeof signatureHeader !== 'string') return false;
if (!rawBody || (Buffer.isBuffer(rawBody) && rawBody.length === 0)) return false;
const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody);
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');
// Oba musia mať rovnakú dĺžku, inak timingSafeEqual hodí chybu
const sig = Buffer.from(signatureHeader);
const exp = Buffer.from(expected);
if (sig.length !== exp.length) return false;
try {
return crypto.timingSafeEqual(sig, exp);
} catch (_) {
return false;
}
}
/**
* Timing-safe porovnanie dvoch tokenov.
*/
function safeCompareTokens(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') return false;
if (a.length !== b.length) return false;
try {
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
} catch (_) {
return false;
}
}
/**
* Express middleware pre admin auth cez `X-Admin-Key` header.
* Vyžaduje env premennú ADMIN_API_KEY.
*/
function adminAuth(req, res, next) {
const expected = process.env.ADMIN_API_KEY;
if (!expected) {
// Ak nie je nastavený, blokujeme všetko (bezpečnejší default)
return res.status(503).json({ error: 'ADMIN_NOT_CONFIGURED', message: 'ADMIN_API_KEY nie je v .env' });
}
const provided = req.headers['x-admin-key'] || req.query.adminKey;
if (!provided || !safeCompareTokens(String(provided), expected)) {
// Phase 2.2: BAD_ADMIN_KEY je 'critical' v abuse-tracker, čo eskaluje IP ban rýchlo.
try {
const abuseTracker = require('./abuse-tracker');
const pool = req.app && req.app.locals && req.app.locals.pool;
if (pool) {
abuseTracker.recordRejection(pool, {
path: req.path, method: req.method, reason: 'BAD_ADMIN_KEY',
http_status: 401, client_ip: req.ip, user_agent: req.headers['user-agent'],
error_detail: provided ? 'wrong key provided' : 'no key provided',
}).catch(() => {});
}
} catch (_) { /* fail silent — never break auth flow */ }
return res.status(401).json({ error: 'UNAUTHORIZED' });
}
next();
}
/**
* CORS whitelist pre KYA-Hub.
*
* - Server-to-server (curl, backend bot): bez hlavičky Origin → vždy povolené (CORS sa netýka).
* - Prehliadač / PWA: Origin musí sedieť na CORS_ALLOWED_ORIGINS (default umbraxon + *.subdomény + localhost).
* - Nepoužívame fixný `allowedHeaders` zoznam: knižnica `cors` potom na OPTIONS zrkadlí
* `Access-Control-Request-Headers` z klienta (X-Pow, X-Admin-Key, …) bez driftu pri nových hlavičkách.
* - Neotvárame Allow-Origin: * s credentials (nepodporované / rizikové); whitelist ostáva.
*/
function buildCorsOptions() {
const raw = process.env.CORS_ALLOWED_ORIGINS || 'https://umbraxon.xyz,https://*.umbraxon.xyz,http://localhost:3000,http://127.0.0.1:3000';
const patterns = raw.split(',').map(s => s.trim()).filter(Boolean);
// Skompiluj patterns na regex (* = wildcard subdomény)
const regexPatterns = patterns.map(p => {
const escaped = p.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
return new RegExp('^' + escaped + '$');
});
return {
origin: function (origin, callback) {
// Žiadny Origin = server-to-server (curl, bot, monitor) → POVOLENÉ
if (!origin) return callback(null, true);
// Browser klient s Origin → musí byť v whitelist
const allowed = regexPatterns.some(re => re.test(origin));
return callback(null, allowed);
},
credentials: true,
methods: ['GET', 'POST', 'OPTIONS'],
maxAge: 86400,
};
}
/**
* Validácia agent name — alphanumeric, hyphens, underscores, dots, 3-64 znakov.
*/
function isValidAgentName(name) {
return typeof name === 'string'
&& name.length >= 3 && name.length <= 64
&& /^[A-Za-z0-9._-]+$/.test(name);
}
/**
* Validácia hex pubkey — 64 alebo 66 znakov (compressed/uncompressed bez 04 prefixu).
*/
function isValidPubkey(pk) {
if (typeof pk !== 'string') return false;
return /^[0-9a-fA-F]{64,130}$/.test(pk);
}
module.exports = {
verifyHmacSignature,
safeCompareTokens,
adminAuth,
buildCorsOptions,
isValidAgentName,
isValidPubkey,
};