Files
CloudSSH/tests/ssh/algorithms.test.ts
T
TanXinandTanXin c9a71f9fa3 test: 完成 v1.0.5 测试覆盖阶段 1+2,修复 3 处安全缺陷并加固 worker 接缝 (#32)
## 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>
2026-07-12 14:11:15 +08:00

192 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
});
});