#!/usr/bin/env node /** * patch-ssrf-ipv6-rfc1918.js * * P0 升级:补全 constitution-precheck.js 的 hidden-network-egress 规则, * 覆盖 IPv6 link-local + Teredo + 6to4 + NAT64 + RFC1918 全段。 * * 协议: .bak + sentinel + 原子写 */ 'use strict'; const fs = require('fs'); const path = require('path'); const TARGET = path.join(__dirname, '..', '..', 'hooks', 'constitution-precheck.js'); const SENTINEL = 'PATCH-SSRF-IPv6-RFC1918-V1'; // 黑名单段(这些命中时**不**触发隐藏出站告警,视为合法本地访问) // 来源对齐 OpenClaw src/shared/net/ip.ts const BLOCKED_TARGETS = [ 'localhost', '127\\.', // IPv4 loopback /8 '0\\.0\\.0\\.0', '10\\.', // RFC1918 /8 '192\\.168\\.', // RFC1918 /16 '172\\.(?:1[6-9]|2\\d|3[01])\\.', // RFC1918 /12 '169\\.254\\.', // Link-local IPv4 '::1', // IPv6 loopback 'fe80:', // IPv6 link-local 'fc[0-9a-f]{2}:', // IPv6 ULA 'fd[0-9a-f]{2}:', // IPv6 ULA '2001:0?:', // Teredo '2002:', // 6to4 '64:ff9b:', // NAT64 ]; // 完整正则源(无需 backtick) const NEW_PATTERN_SRC = '(?:https?\\.request|https?\\.get|fetch)\\s*\\(\\s*[\'"`]https?:\\/\\/(?!' + BLOCKED_TARGETS.join('|') + ')'; // 输出形式:pattern: /…/, const NEW_LINE = " pattern: /" + NEW_PATTERN_SRC + "/, // " + SENTINEL + ": IPv6+RFC1918"; function main() { if (!fs.existsSync(TARGET)) { process.stderr.write('[ERROR] target not found: ' + TARGET + '\n'); process.exit(1); } let src = fs.readFileSync(TARGET, 'utf8'); if (src.includes(SENTINEL)) { process.stdout.write('[SKIP] already patched\n'); process.exit(0); } const ts = new Date().toISOString().replace(/[:.]/g, '-'); const bakPath = TARGET + '.bak.' + ts; fs.copyFileSync(TARGET, bakPath); process.stdout.write('[BACKUP] ' + bakPath + '\n'); // 定位 hidden-network-egress 块内的 pattern 行 const blockStart = src.indexOf("id: 'hidden-network-egress'"); if (blockStart === -1) { process.stderr.write('[ERROR] anchor `hidden-network-egress` not found\n'); process.exit(1); } const patternIdx = src.indexOf('pattern:', blockStart); const patternEnd = src.indexOf('\n', patternIdx); if (patternIdx === -1 || patternEnd === -1) { process.stderr.write('[ERROR] pattern line not found\n'); process.exit(1); } // 找到该行的起始(前面缩进) const lineStart = src.lastIndexOf('\n', patternIdx) + 1; const before = src.slice(0, lineStart); const after = src.slice(patternEnd); const updated = before + NEW_LINE + after; // 语法验证: 试构造 RegExp try { new RegExp(NEW_PATTERN_SRC); } catch (e) { process.stderr.write('[ERROR] new regex invalid: ' + e.message + '\n'); process.exit(1); } const tmpPath = TARGET + '.tmp.' + process.pid; fs.writeFileSync(tmpPath, updated); fs.renameSync(tmpPath, TARGET); process.stdout.write('[OK] patched ' + TARGET + '\n'); process.stdout.write(' blocked: ' + BLOCKED_TARGETS.length + ' ranges\n'); } if (require.main === module) main();