## Summary - **修复 3 个真实生产缺陷**:ssrf.ts IPv6 `[::1]` 方括号未剥离导致 SSRF 绕过、safety.ts `apk add/del` 子命令漏覆盖、crypto.ts catch 块 `ciphertext.length` 在 null 输入时二次崩溃 - **完成 SSH 协议层阶段 1+2 测试覆盖**:新增 7 个测试文件、347 用例,覆盖 safety / ssrf / algorithms / kex / crypto / packet / utils 核心模块,crypto.ts 与 utils.ts 达到 100% 覆盖率 - **新增 worker 接缝安全测试**:12 用例覆盖 OAuth CSRF、IDOR 越权、SSRF 接缝、cf_verified 签名伪造、跨站 WebSocket 劫持(CSWSH)五类高危边界 - **发布 v1.0.5**,更新 CHANGELOG 与 package.json 版本号 ## 变更明细 | 类别 | 文件 | 用例数 | 覆盖率 | |------|------|--------|--------| | 安全模块 | `tests/worker/agent/safety.test.ts` | 86 | 双路径全覆盖 | | 安全模块 | `tests/worker/agent/ssrf.test.ts` | 28 | 私有地址段全覆盖 | | 协议层 | `tests/ssh/algorithms.test.ts` | 33 | — | | 协议层 | `tests/ssh/kex.test.ts` | 26 | — | | 协议层 | `tests/ssh/crypto.test.ts` | 56 | **100%** | | 协议层 | `tests/ssh/packet.test.ts` | 49 | **96%** | | 协议层 | `tests/ssh/utils.test.ts` | 55 | **100%** | | 接缝安全 | `tests/worker/security.test.ts` | 12 | 5 类高危边界 | **累计**:11 个测试文件,371 用例全部通过,耗时 <1 秒。 ## 修复的 3 个缺陷详情 1. **ssrf.ts — IPv6 `[::1]` 绕过(高危)**:`validateBaseUrl` 未剥离方括号,用户可将 AI base_url 设为 `[::1]:8080` 指向本机,绕过本机/内网拦截,可能导致 Agent 走私到内网或泄露云元数据凭据 2. **safety.ts — `apk` 子命令漏覆盖(中危)**:`needsConfirmation` 正则仅覆盖 apt/apt-get,未覆盖 Alpine 系 `apk add/del`,可静默安装/卸载系统包而无需确认 3. **crypto.ts — 异常路径二次崩溃(高危)**:catch 块读取 `ciphertext.length` 在 null 输入时自身 throw,导致 graceful degradation 失效,错误处理路径反而制造错误 ## Test Plan - [x] `pnpm test` — 11 文件 / 371 用例 / 全绿 / 919ms - [x] 验证 ssrf.ts `[::1]` 不再绕过 - [x] 验证 safety.ts `apk add` 触发确认 - [x] 验证 crypto.ts null 输入进入 catch 返回 null 而非 throw ## Scope Note 阶段 3/4(Agent 控制循环、SSH 会话、Durable Objects、前端)计划不纳入单元测试,有状态组件 + 外部 IO 的单测维护成本接偏高,回报率低,改由人工测试 + 用户反馈循环兜底。本 PR 仅守住纯函数与关键接缝的安全边界。 --------- Co-authored-by: TanXin <tanxin@example.com>
192 lines
7.7 KiB
TypeScript
192 lines
7.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import {
|
||
KEX_ALGORITHM_CURVE25519_SHA256,
|
||
KEX_ALGORITHM_ECDH_NISTP256,
|
||
SUPPORTED_KEX_ALGORITHMS,
|
||
SUPPORTED_ENCRYPTION_ALGORITHMS,
|
||
SUPPORTED_MAC_ALGORITHMS,
|
||
isCurve25519KEXAlgorithm,
|
||
getCipherSpec,
|
||
getMacSpec,
|
||
getMacAlgorithmsForCipher,
|
||
} from '../../src/ssh/algorithms';
|
||
|
||
// =====================================================================
|
||
// algorithms.test.ts
|
||
// ---------------------------------------------------------------
|
||
// SSH 算法名常量与查表函数的定义层。项目里多处 KEX/Cipher/MAC
|
||
// 协商都依赖这张表,查表抛错或返回错误 spec 会让 SSH 握手静默走错
|
||
// 加密通道,所以把每条规则的语义用回归测试固化下来。
|
||
// =====================================================================
|
||
|
||
describe('algorithms — 常量定义', () => {
|
||
it('KEX 算法常量值符合 RFC 8732 / IANA 命名', () => {
|
||
expect(KEX_ALGORITHM_CURVE25519_SHA256).toBe('curve25519-sha256');
|
||
expect(KEX_ALGORITHM_ECDH_NISTP256).toBe('ecdh-sha2-nistp256');
|
||
});
|
||
|
||
it('SUPPORTED_KEX_ALGORITHMS 把 curve25519 排在前面(优先协商)', () => {
|
||
expect(SUPPORTED_KEX_ALGORITHMS[0]).toBe(KEX_ALGORITHM_CURVE25519_SHA256);
|
||
expect(SUPPORTED_KEX_ALGORITHMS).toContain(KEX_ALGORITHM_ECDH_NISTP256);
|
||
expect(SUPPORTED_KEX_ALGORITHMS.length).toBeGreaterThanOrEqual(2);
|
||
});
|
||
|
||
it('SUPPORTED_ENCRYPTION_ALGORITHMS 包含 GCM 与 CTR 两族', () => {
|
||
expect(SUPPORTED_ENCRYPTION_ALGORITHMS).toContain('aes256-gcm@openssh.com');
|
||
expect(SUPPORTED_ENCRYPTION_ALGORITHMS).toContain('aes128-gcm@openssh.com');
|
||
expect(SUPPORTED_ENCRYPTION_ALGORITHMS).toContain('aes256-ctr');
|
||
expect(SUPPORTED_ENCRYPTION_ALGORITHMS).toContain('aes128-ctr');
|
||
});
|
||
|
||
it('SUPPORTED_MAC_ALGORITHMS 覆盖 SHA2 家族', () => {
|
||
expect(SUPPORTED_MAC_ALGORITHMS).toContain('hmac-sha2-256');
|
||
expect(SUPPORTED_MAC_ALGORITHMS).toContain('hmac-sha2-512');
|
||
expect(SUPPORTED_MAC_ALGORITHMS).toContain('hmac-sha1');
|
||
});
|
||
});
|
||
|
||
describe('algorithms — isCurve25519KEXAlgorithm', () => {
|
||
it('识别 curve25519-sha256', () => {
|
||
expect(isCurve25519KEXAlgorithm('curve25519-sha256')).toBe(true);
|
||
});
|
||
it('识别非 curve25519 算法', () => {
|
||
expect(isCurve25519KEXAlgorithm('ecdh-sha2-nistp256')).toBe(false);
|
||
});
|
||
it('不识别大小写变体(SSH 算法名大小写敏感)', () => {
|
||
expect(isCurve25519KEXAlgorithm('Curve25519-SHA256')).toBe(false);
|
||
});
|
||
it('不识别空字符串/未实现算法', () => {
|
||
expect(isCurve25519KEXAlgorithm('')).toBe(false);
|
||
expect(isCurve25519KEXAlgorithm('diffie-hellman-group14-sha256')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('algorithms — getCipherSpec', () => {
|
||
it('返回 aes256-gcm@openssh.com 的正确 spec', () => {
|
||
const spec = getCipherSpec('aes256-gcm@openssh.com');
|
||
expect(spec.mode).toBe('gcm');
|
||
expect(spec.blockSize).toBe(16);
|
||
expect(spec.ivLength).toBe(12);
|
||
expect(spec.keyLength).toBe(32);
|
||
expect(spec.aead).toBe(true);
|
||
});
|
||
it('返回 aes128-gcm@openssh.com 的正确 spec', () => {
|
||
const spec = getCipherSpec('aes128-gcm@openssh.com');
|
||
expect(spec.mode).toBe('gcm');
|
||
expect(spec.blockSize).toBe(16);
|
||
expect(spec.ivLength).toBe(12);
|
||
expect(spec.keyLength).toBe(16);
|
||
expect(spec.aead).toBe(true);
|
||
});
|
||
it('返回 aes256-ctr 的正确 spec(CTR 非 AEAD)', () => {
|
||
const spec = getCipherSpec('aes256-ctr');
|
||
expect(spec.mode).toBe('ctr');
|
||
expect(spec.blockSize).toBe(16);
|
||
expect(spec.ivLength).toBe(16);
|
||
expect(spec.keyLength).toBe(32);
|
||
expect(spec.aead).toBe(false);
|
||
});
|
||
it('返回 aes192-ctr 的正确 spec(24 字节密钥)', () => {
|
||
const spec = getCipherSpec('aes192-ctr');
|
||
expect(spec.keyLength).toBe(24);
|
||
expect(spec.aead).toBe(false);
|
||
});
|
||
it('返回 aes128-ctr 的正确 spec', () => {
|
||
const spec = getCipherSpec('aes128-ctr');
|
||
expect(spec.keyLength).toBe(16);
|
||
expect(spec.aead).toBe(false);
|
||
});
|
||
it('对未知 cipher 抛错且消息包含算法名', () => {
|
||
expect(() => getCipherSpec('aes512-ctr')).toThrow(/Unsupported cipher.*aes512-ctr/);
|
||
});
|
||
it('对空字符串抛错', () => {
|
||
expect(() => getCipherSpec('')).toThrow(/Unsupported cipher/);
|
||
});
|
||
});
|
||
|
||
describe('algorithms — getMacSpec', () => {
|
||
it('返回 hmac-sha2-256 的正确 spec(32 字节)', () => {
|
||
const spec = getMacSpec('hmac-sha2-256');
|
||
expect(spec.length).toBe(32);
|
||
expect(spec.keyLength).toBe(32);
|
||
});
|
||
it('返回 hmac-sha2-512 的正确 spec(64 字节)', () => {
|
||
const spec = getMacSpec('hmac-sha2-512');
|
||
expect(spec.length).toBe(64);
|
||
expect(spec.keyLength).toBe(64);
|
||
});
|
||
it('返回 hmac-sha1 的正确 spec(20 字节,遗留兼容)', () => {
|
||
const spec = getMacSpec('hmac-sha1');
|
||
expect(spec.length).toBe(20);
|
||
expect(spec.keyLength).toBe(20);
|
||
});
|
||
it('返回 none 的 spec(AEAD 模式占位)', () => {
|
||
const spec = getMacSpec('none');
|
||
expect(spec.length).toBe(0);
|
||
expect(spec.keyLength).toBe(32);
|
||
});
|
||
it('对未知 MAC 抛错且消息包含算法名', () => {
|
||
expect(() => getMacSpec('hmac-md5')).toThrow(/Unsupported MAC.*hmac-md5/);
|
||
});
|
||
it('对空字符串抛错', () => {
|
||
expect(() => getMacSpec('')).toThrow(/Unsupported MAC/);
|
||
});
|
||
});
|
||
|
||
describe('algorithms — getMacAlgorithmsForCipher', () => {
|
||
it('AEAD cipher (aes256-gcm@openssh.com) 应返回 ["none"]', () => {
|
||
// GCM 自带完整性校验,不需要独立 MAC
|
||
expect(getMacAlgorithmsForCipher('aes256-gcm@openssh.com')).toEqual(['none']);
|
||
});
|
||
it('AEAD cipher (aes128-gcm@openssh.com) 应返回 ["none"]', () => {
|
||
expect(getMacAlgorithmsForCipher('aes128-gcm@openssh.com')).toEqual(['none']);
|
||
});
|
||
it('CTR cipher (aes256-ctr) 应返回完整 MAC 列表(需要独立 MAC)', () => {
|
||
const macs = getMacAlgorithmsForCipher('aes256-ctr');
|
||
expect(macs).toEqual(SUPPORTED_MAC_ALGORITHMS);
|
||
expect(macs).toContain('hmac-sha2-256');
|
||
expect(macs).toContain('hmac-sha2-512');
|
||
expect(macs).not.toContain('none');
|
||
});
|
||
it('CTR cipher (aes128-ctr) 同样返回完整 MAC 列表', () => {
|
||
expect(getMacAlgorithmsForCipher('aes128-ctr')).toEqual(SUPPORTED_MAC_ALGORITHMS);
|
||
});
|
||
it('传入未知 cipher 时通过 getCipherSpec 抛错', () => {
|
||
expect(() => getMacAlgorithmsForCipher('aes512-ctr')).toThrow(/Unsupported cipher/);
|
||
});
|
||
});
|
||
|
||
describe('algorithms — CipherSpec 类型不变性', () => {
|
||
// 把所有支持的 cipher 各跑一遍,固化"必填字段都要存在"这一约束,
|
||
// 防止未来新增 cipher 时漏字段。
|
||
for (const algo of SUPPORTED_ENCRYPTION_ALGORITHMS) {
|
||
it(`${algo} 的 spec 字段都齐全`, () => {
|
||
const spec = getCipherSpec(algo);
|
||
expect(typeof spec.mode).toBe('string');
|
||
expect(['gcm', 'ctr']).toContain(spec.mode);
|
||
expect(typeof spec.blockSize).toBe('number');
|
||
expect(typeof spec.ivLength).toBe('number');
|
||
expect(typeof spec.keyLength).toBe('number');
|
||
expect(typeof spec.aead).toBe('boolean');
|
||
});
|
||
}
|
||
|
||
// GCM 必须是 AEAD=true,CTR 必须是 AEAD=false —— 这两条不变性一旦错位
|
||
// 会直接导致 MAC 校验逻辑走错分支(GCM 漏校验 MAC,或 CTR 多挂一个
|
||
// 不存在的 AEAD tag 处理),属于协议级灾难,单独固定。
|
||
it('所有 GCM cipher 的 aead 必为 true', () => {
|
||
for (const algo of SUPPORTED_ENCRYPTION_ALGORITHMS) {
|
||
if (algo.includes('gcm')) {
|
||
expect(getCipherSpec(algo).aead, algo).toBe(true);
|
||
}
|
||
}
|
||
});
|
||
it('所有 CTR cipher 的 aead 必为 false', () => {
|
||
for (const algo of SUPPORTED_ENCRYPTION_ALGORITHMS) {
|
||
if (algo.includes('ctr')) {
|
||
expect(getCipherSpec(algo).aead, algo).toBe(false);
|
||
}
|
||
}
|
||
});
|
||
});
|