- VERSION file as authoritative version source - export.mjs reads VERSION with package.json fallback - bw-ota.ps1 DryRun mode for safe testing - auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
98 lines
3.3 KiB
JavaScript
98 lines
3.3 KiB
JavaScript
#!/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();
|