diff --git a/aibox/skills/aes-crypto.py b/aibox/skills/aes-crypto.py index ace802c..a873e5f 100755 --- a/aibox/skills/aes-crypto.py +++ b/aibox/skills/aes-crypto.py @@ -59,14 +59,14 @@ class AESCli: return False def generate_random_key(self, key_size=16): - """生成随机密钥""" + """生成随机密钥(S8:密钥内容不打印,避免进入会话日志;经 --save-key 持久化)""" if key_size not in [16, 24, 32]: print(f"错误: 密钥大小必须为16、24或32,当前为{key_size}") return None self.key = get_random_bytes(key_size) key_hex = self.key.hex() - print(f"已生成 {key_size} 字节随机密钥: {key_hex}") + print(f"已生成 {key_size} 字节随机密钥(内容不显示,请用 --save-key 保存到文件)") return key_hex def generate_random_iv(self): @@ -330,6 +330,10 @@ class AESCli: with open(filepath, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) + try: + os.chmod(filepath, 0o600) # S8:密钥文件仅属主可读写 + except OSError: + pass # 不支持 POSIX 权限的平台(如 Windows)跳过 print(f"密钥和IV已保存到: {filepath}") return True @@ -395,38 +399,33 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 使用示例: - # 生成随机密钥和IV - aes-crypto --gen-key + # 生成随机密钥和IV(密钥不打印,经 --save-key 保存到文件) + aes-crypto --gen-key --gen-iv --save-key keys.json aes-crypto --gen-iv - # 文本加解密 - aes-crypto --key <密钥hex> --iv --encrypt-text "Hello World" - aes-crypto --key <密钥hex> --iv --decrypt-text "<密文base64>" + # 文本加解密(密钥经文件提供,不走命令行) + aes-crypto --key-file key.txt --iv --encrypt-text "Hello World" + aes-crypto --key-file key.txt --iv --decrypt-text "<密文base64>" # 文件加解密 - aes-crypto --key <密钥hex> --iv --encrypt-file input.txt - aes-crypto --key <密钥hex> --iv --decrypt-file input.txt.encrypted + aes-crypto --load-key keys.json --encrypt-file input.txt + aes-crypto --load-key keys.json --decrypt-file input.txt.encrypted # 批量加解密 - aes-crypto --key <密钥hex> --iv --batch-encrypt file1.txt file2.txt - aes-crypto --key <密钥hex> --iv --batch-decrypt *.encrypted - - # 保存和加载密钥 - aes-crypto --key <密钥hex> --iv --save-key keys.json - aes-crypto --load-key keys.json --encrypt-file input.txt + aes-crypto --load-key keys.json --batch-encrypt file1.txt file2.txt + aes-crypto --load-key keys.json --batch-decrypt *.encrypted # 使用GCM模式 - aes-crypto --mode GCM --gen-key --key-size 32 - aes-crypto --mode GCM --key <密钥hex> --iv --encrypt-file input.txt + aes-crypto --mode GCM --gen-key --key-size 32 --save-key keys.json + aes-crypto --mode GCM --load-key keys.json --encrypt-file input.txt # 从文件读取密钥 aes-crypto --key-file key.txt --iv --encrypt-file input.txt 参数说明: --mode CBC|GCM 加密模式 (默认: CBC) - --key KEY 十六进制密钥 (16/24/32字节) --iv IV 十六进制IV/Nonce (CBC:16字节, GCM:12字节) - --key-file FILE 从文件读取密钥 + --key-file FILE 从文件读取密钥(密钥仅经文件提供,避免进入进程列表与会话日志) --load-key FILE 从JSON文件加载密钥和IV --save-key FILE 保存密钥和IV到JSON文件 --gen-key 生成随机密钥 @@ -448,8 +447,7 @@ def main(): parser.add_argument('--mode', choices=['CBC', 'GCM'], default='CBC', help='加密模式 (默认: CBC)') - # 密钥和IV设置 - parser.add_argument('--key', help='十六进制密钥 (16/24/32字节)') + # 密钥和IV设置(S7:密钥仅经 --key-file/--load-key 文件途径提供,不进命令行) parser.add_argument('--iv', help='十六进制IV/Nonce (CBC:16字节, GCM:12字节)') parser.add_argument('--key-file', help='从文件读取密钥') parser.add_argument('--load-key', help='从JSON文件加载密钥和IV') @@ -491,27 +489,28 @@ def main(): sys.exit(1) # 处理密钥文件 + key_input = None if args.key_file: try: with open(args.key_file, 'r') as f: - args.key = f.read().strip() + key_input = f.read().strip() except Exception as e: print(f"错误: 读取密钥文件失败: {e}") sys.exit(1) # 设置密钥和IV - if args.key: + if key_input: if args.iv is None and not aes.iv: print("错误: 需要同时提供密钥和IV/Nonce") sys.exit(1) if args.iv: - if not aes.set_key_iv(args.key, args.iv): + if not aes.set_key_iv(key_input, args.iv): sys.exit(1) elif aes.iv: # 从加载的密钥文件中获取IV try: - aes.key = bytes.fromhex(args.key) + aes.key = bytes.fromhex(key_input) if len(aes.key) not in [16, 24, 32]: print(f"错误: 密钥长度必须为16、24或32字节,当前为{len(aes.key)}字节") sys.exit(1) @@ -548,7 +547,7 @@ def main(): if args.encrypt_text or args.decrypt_text or \ args.encrypt_file or args.decrypt_file or \ args.batch_encrypt or args.batch_decrypt: - print("错误: 请先设置密钥和IV/Nonce (使用 --key 和 --iv 或 --load-key)") + print("错误: 请先设置密钥和IV/Nonce (使用 --key-file 和 --iv 或 --load-key)") sys.exit(1) try: diff --git a/aibox/skills/gitclient.py b/aibox/skills/gitclient.py index 9dcf0b1..015cc8b 100755 --- a/aibox/skills/gitclient.py +++ b/aibox/skills/gitclient.py @@ -79,19 +79,30 @@ class GitHubTool: return date_str def _run_git(self, repo_path: str, cmd: List[str]) -> Dict: + env = {**os.environ, "GIT_TERMINAL_PROMPT": "0"} + if GITHUB_TOKEN: + # 凭据仅经环境变量传递(git>=2.31 的 GIT_CONFIG_* 机制), + # 不进命令行 argv / 远端 URL / .git/config(S1,对齐 J12 条例) + env["GIT_CONFIG_COUNT"] = "1" + env["GIT_CONFIG_KEY_0"] = "http.extraheader" + env["GIT_CONFIG_VALUE_0"] = f"Authorization: token {GITHUB_TOKEN}" try: result = subprocess.run( ["git"] + cmd, cwd=repo_path, capture_output=True, - text=True, timeout=60, env={**os.environ, "GIT_TERMINAL_PROMPT": "0"} + text=True, timeout=60, env=env ) - return {"success": result.returncode == 0, "stdout": result.stdout.strip(), "stderr": result.stderr.strip()} + return {"success": result.returncode == 0, + "stdout": self._scrub_token(result.stdout.strip()), + "stderr": self._scrub_token(result.stderr.strip())} except Exception as e: - return {"success": False, "error": str(e)} + return {"success": False, "error": self._scrub_token(str(e))} - def _auth_url(self, repo_url: str) -> str: - if GITHUB_TOKEN and repo_url.startswith("https://"): - return repo_url.replace("https://", f"https://{GITHUB_TOKEN}@") - return repo_url + @staticmethod + def _scrub_token(text: str) -> str: + """输出脱敏:任何路径回显的 token 一律替换(S1)""" + if GITHUB_TOKEN and text: + return text.replace(GITHUB_TOKEN, "***") + return text # ==================== 搜索功能 ==================== @@ -154,7 +165,7 @@ class GitHubTool: if os.path.exists(target_dir): return f"⚠️ 仓库已存在: {target_dir}\n💡 使用 pull 命令拉取更新" - clone_url = self._auth_url(f"https://github.com/{repo}.git") + clone_url = f"https://github.com/{repo}.git" cmd = ["clone"] if depth > 0: cmd.extend(["--depth", str(depth)]) diff --git a/aibox/skills/sftp-transfer.py b/aibox/skills/sftp-transfer.py index 44d0a10..105767f 100755 --- a/aibox/skills/sftp-transfer.py +++ b/aibox/skills/sftp-transfer.py @@ -27,9 +27,12 @@ def build_parser(): --host SSH服务器主机名或IP地址 (必填) --port SSH服务器端口号,默认 22 -u, --username SSH登录用户名 (必填) - -p, --password SSH登录密码 (如不提供则交互式输入) --key SSH私钥文件路径 (用于密钥认证) + 密码认证: + 密码经环境变量 AIBOX_SFTP_PASS 提供;未设置且未用 --key 时 + 交互式输入。不走命令行参数,避免凭据进入进程列表与会话日志(J12 条例)。 + 操作参数: --upload LOCAL REMOTE 上传:本地文件/目录 -> 远程服务器路径 --download REMOTE LOCAL 下载:远程服务器文件/目录 -> 本地路径 @@ -37,19 +40,19 @@ def build_parser(): -v, --verbose 显示详细信息 使用示例: - 1. 上传文件(密码认证): - sftp-transfer --host 192.168.1.100 -u root -p mypass --upload /tmp/test.txt /root/ + 1. 上传文件(密码认证,环境变量提供密码): + AIBOX_SFTP_PASS=mypass sftp-transfer --host 192.168.1.100 -u root --upload /tmp/test.txt /root/ 2. 下载文件(密钥认证): sftp-transfer --host 192.168.1.100 -u root --key ~/.ssh/id_rsa \\ --download /root/test.txt /tmp/ 3. 递归上传目录: - sftp-transfer --host 192.168.1.100 -u user -p pass -r \\ + AIBOX_SFTP_PASS=pass sftp-transfer --host 192.168.1.100 -u user -r \\ --upload ./mydir /home/user/mydir 4. 下载目录: - sftp-transfer --host 192.168.1.100 -u user -p pass -r \\ + AIBOX_SFTP_PASS=pass sftp-transfer --host 192.168.1.100 -u user -r \\ --download /home/user/backup ./backup 5. 交互式输入密码: @@ -61,7 +64,6 @@ def build_parser(): parser.add_argument('--host', required=True, help='SSH服务器主机名或IP地址') parser.add_argument('--port', type=int, default=22, help='SSH服务器端口号,默认22') parser.add_argument('-u', '--username', required=True, help='SSH登录用户名') - parser.add_argument('-p', '--password', help='SSH登录密码(如不提供则交互式输入)') parser.add_argument('--key', help='SSH私钥文件路径(用于密钥认证)') # 操作参数 @@ -82,9 +84,13 @@ def log(verbose, msg): def get_password(args): - """获取密码:优先从命令行参数获取,否则交互式输入""" - if args.password: - return args.password + """获取密码:环境变量 AIBOX_SFTP_PASS 优先,否则交互式输入(S5:凭据不走命令行)""" + env_pass = os.environ.get("AIBOX_SFTP_PASS") + if env_pass: + return env_pass + if args.key: + # 密钥认证时不强制索要密码(仅在私钥有口令时经交互输入) + return None return getpass.getpass(f"请输入 {args.username}@{args.host} 的密码: ") diff --git a/aibox/skills/ssh-exec.py b/aibox/skills/ssh-exec.py index dce6036..857bc8e 100755 --- a/aibox/skills/ssh-exec.py +++ b/aibox/skills/ssh-exec.py @@ -2,6 +2,7 @@ """ssh-exec - 通过SSH远程执行单条命令的工具(支持密码认证)""" import argparse +import os import sys import paramiko @@ -14,24 +15,32 @@ def main(): -H, --host SSH主机地址(默认: 127.0.0.1) -P, --port SSH端口号(默认: 22) -u, --user SSH用户名(必填) - -p, --password SSH密码(必填) -c, --command 要执行的远程命令(必填) +密码认证: + 密码经环境变量 AIBOX_SSH_PASS 提供(必填),不走命令行参数, + 避免凭据进入进程列表与会话日志(J12 条例)。 + 使用示例: - %(prog)s -u qing -p 12345678 -c "ls -la" - %(prog)s -u root -H 192.168.1.100 -P 22 -p mypass -c "df -h" - %(prog)s -u qing -H 127.0.0.1 -p 12345678 -c "uname -a" + AIBOX_SSH_PASS=12345678 %(prog)s -u qing -c "ls -la" + AIBOX_SSH_PASS=mypass %(prog)s -u root -H 192.168.1.100 -P 22 -c "df -h" + AIBOX_SSH_PASS=12345678 %(prog)s -u qing -H 127.0.0.1 -c "uname -a" """, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("-H", "--host", default="127.0.0.1", help="SSH主机地址(默认: 127.0.0.1)") parser.add_argument("-P", "--port", type=int, default=22, help="SSH端口号(默认: 22)") parser.add_argument("-u", "--user", required=True, help="SSH用户名(必填)") - parser.add_argument("-p", "--password", required=True, help="SSH密码(必填)") parser.add_argument("-c", "--command", required=True, help="要执行的远程命令(必填)") args = parser.parse_args() + # 密码仅从环境变量读取(S4:凭据不进命令行) + password = os.environ.get("AIBOX_SSH_PASS") + if not password: + print("错误: 请通过环境变量 AIBOX_SSH_PASS 提供SSH密码", file=sys.stderr) + sys.exit(2) + client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) @@ -40,7 +49,7 @@ def main(): hostname=args.host, port=args.port, username=args.user, - password=args.password, + password=password, timeout=10, ) diff --git a/docs/reports/aibox_audit_20260817.md b/docs/reports/aibox_audit_20260817.md index 5d4b98d..c1600ec 100644 --- a/docs/reports/aibox_audit_20260817.md +++ b/docs/reports/aibox_audit_20260817.md @@ -1,9 +1,9 @@ # aibox 模块安全与质量审计报告 -- **日期**:2026-08-17(第一轮)/ 2026-08-18(第二轮复审)/ 2026-08-18(第三轮:遗留复核 + v3.4 专审)/ 2026-08-18(第四轮:3 路并行审读 + Batch H 修复)/ 2026-08-19(第五轮:4 路并行审读,聚焦历届未覆盖区域)/ 2026-08-20(第六轮:交叉视角——修复回归验证 / 并发与生命周期 / 降级路径 / 修复间相互作用) -- **审计基线**:第一轮 HEAD `ce142fe`(Batch A–D 后)→ 第二轮 HEAD `177a9ac`(Batch E 修复后)→ 第三轮 HEAD `aa15841`(L4a/b/c 落地后)→ 第四轮 HEAD `d1f780e`(Batch G + SOCKS5 验证后)→ 第五轮 HEAD `d513c5f`(Batch I + model_config.py 后,含工作区未提交的 max_turns 钳制)→ 第六轮 HEAD `7dc6796`(Batch K 全落地 + 五轮收束文档同步后) -- **审计范围**:`aibox.py`(~3458 行)+ `llm_client.py` + `model_config.py` + `tools/`(7 文件)+ `subsystems/`(6 文件)+ `common/`(5 文件)+ 前端 HTML(frontend.html / entry.html / entry1.html)+ `skills/` 脚本 + `client.py` / `voice_client.py`(前端与技能脚本为第五轮首次覆盖) -- **方法**:第一轮逐文件代码审读 + PoC 动态验证;第二轮 Batch E 修复复审 + 全量重审 + 6 处关键发现实证抽查;第三轮遗留项复核 + v3.4 专审;第四轮 3 路并行审读 + 主审逐条复核 + Linux PoC 实证(6/6);第五轮 4 路并行审读(聚焦历届未覆盖区域)+ 主审逐条复核 + Linux PoC 实证(2/2);第六轮 4 路并行审读(Batch K 修复回归验证与修复自身缺陷面 / 并发与资源生命周期交叉 / 数据边界与故障降级 / 工具链与修复间相互作用)+ 主审逐条代码复核(Windows 开发机,无 Linux PoC,动态行为以代码级核验 + Windows 测试基线代替) +- **日期**:2026-08-17(第一轮)/ 2026-08-18(第二轮复审)/ 2026-08-18(第三轮:遗留复核 + v3.4 专审)/ 2026-08-18(第四轮:3 路并行审读 + Batch H 修复)/ 2026-08-19(第五轮:4 路并行审读,聚焦历届未覆盖区域)/ 2026-08-20(第六轮:交叉视角——修复回归验证 / 并发与生命周期 / 降级路径 / 修复间相互作用)/ 2026-08-20(第七轮:skills 全量脚本 / 前端深维度 / 客户端三文件 / WS+CLI 命令面全量校验) +- **审计基线**:第一轮 HEAD `ce142fe`(Batch A–D 后)→ 第二轮 HEAD `177a9ac`(Batch E 修复后)→ 第三轮 HEAD `aa15841`(L4a/b/c 落地后)→ 第四轮 HEAD `d1f780e`(Batch G + SOCKS5 验证后)→ 第五轮 HEAD `d513c5f`(Batch I + model_config.py 后,含工作区未提交的 max_turns 钳制)→ 第六轮 HEAD `7dc6796`(Batch K 全落地 + 五轮收束文档同步后)→ 第七轮 HEAD `e43aeea`(Batch L 落地后) +- **审计范围**:`aibox.py`(~3458 行)+ `llm_client.py` + `model_config.py` + `tools/`(7 文件)+ `subsystems/`(6 文件)+ `common/`(5 文件)+ 前端 HTML(frontend.html / entry.html / entry1.html)+ `skills/` 脚本 + `client.py` / `voice_client.py`(前端与技能脚本为第五轮首次覆盖;第七轮完成 skills 全量逐文件与命令面逐条覆盖) +- **方法**:第一轮逐文件代码审读 + PoC 动态验证;第二轮 Batch E 修复复审 + 全量重审 + 6 处关键发现实证抽查;第三轮遗留项复核 + v3.4 专审;第四轮 3 路并行审读 + 主审逐条复核 + Linux PoC 实证(6/6);第五轮 4 路并行审读(聚焦历届未覆盖区域)+ 主审逐条复核 + Linux PoC 实证(2/2);第六轮 4 路并行审读(Batch K 修复回归验证与修复自身缺陷面 / 并发与资源生命周期交叉 / 数据边界与故障降级 / 工具链与修复间相互作用)+ 主审逐条代码复核(Windows 开发机,无 Linux PoC,动态行为以代码级核验 + Windows 测试基线代替);第七轮 4 路并行审读(skills 全量安全面 / 前端深维度 / model_config+voice_client+client 深审 / WS+CLI 命令面全量逐条校验)+ 主审对全部中危项逐条代码实证(Windows 开发机) ### 轮次总览 @@ -15,6 +15,7 @@ | 第四轮 | 2026-08-18 | `d1f780e` | Batch H 全项(含 N5 取消传播回归,高危) | Batch H + Batch I 遗留收尾 | 424 passed | | 第五轮 | 2026-08-19 | `d513c5f` | J1~J16(高危 2 / 中危 5 / 低危与信息 9) | Batch K(波次 1~5 全落地) | 490 passed | | 第六轮 | 2026-08-20 | `7dc6796` | R1~R3(均低危;Batch K 回归验证零回归) | Batch L(R1~R3 全落地) | 490 passed(Linux)/ 493 passed(Windows) | +| 第七轮 | 2026-08-20 | `e43aeea` | 46 项(高危 0 / 中危 11 / 低危 26 / 信息 9;Batch L 回归验证零回归) | Batch M 波次 1 已落地(S1/S4/S5/S7/S8),波次 2~5 待落地 | 493 passed / 4 failed(平台环境项)/ 1 skipped(Windows) | --- @@ -32,7 +33,9 @@ **第六轮**(基线 7dc6796):交叉验证轮——Batch K 修复回归验证(J1~J16 逐项在位)、并发与生命周期、降级路径、修复间相互作用四路审读。**零回归、无修复引入的缺陷**;新发现 R1~R3 均为低危(`/memos` 裸 strptime 加固漏点 / handle_interrupt 全局 executor 取消的架构残留 / broadcast 超时后半开连接有界泄漏),已随 **Batch L 全部修复落地**。见"十六、第六轮审计"。 -**最终状态**:六轮累计发现 72 项(高危 7 / 中危 26 / 低危与信息 39),**全部修复落地**(Batch E/F/G/H/I/K/L),无未修复安全发现;维持原判仅限设计决策项,见"十五、最终状态与遗留清单"与"十六、第六轮审计"。 +**第七轮**(基线 e43aeea):四路新维度——skills 全量 29 脚本逐文件(继 J12 点修后首次)、前端三页面深维度、model_config/voice_client/client 深审、WS+CLI 命令面逐条校验;主审对全部中危项逐条代码实证。Batch L 零回归;新发现 **46 项**(高危 0 / 中危 11 / 低危 26 / 信息 9),集中于 skills 凭据进命令行(S1/S4/S5/S7,违反项目自定 J12 条例)、skills 路径边界(S2/S9/S11/S12,含 vision.py 任意文件经云 API 渗出的提示注入通道)、前端功能缺陷(F1 清空按钮必抛 TypeError / F2 手动断开被重连复活)、voice_client input 线程泄漏(M1)与命令面输入规整(W 系列)。Batch M 修复建议(5 波次)已给出,待落地。见"十七、第七轮审计"。 + +**最终状态**:七轮累计发现 118 项(高危 7 / 中危 37 / 低危与信息 74);前六轮 72 项**全部修复落地**(Batch E/F/G/H/I/K/L),第七轮 **Batch M 波次 1 已落地**(S1/S4/S5/S7/S8 凭据收口,见"十八"),波次 2~5(41 项,中危 6 / 低危与信息 35)待落地;维持原判仅限设计决策项,见"十五、最终状态与遗留清单"与"十六、第六轮审计"。 --- @@ -731,3 +734,159 @@ Linux 基线(`aibox/venv`)维持 **490 passed** 不受影响。 ### 16.5 第六轮结论 交叉验证轮:**Batch K 修复零回归**(J1~J16 逐项在位,历届 E~I 修复抽查均在位),**无修复引入的回归**;新发现 R1~R3 均为低危边界项(加固漏点/架构残留/J7 残留面),无高危、无中危,**已随 Batch L 全部修复落地**。六轮累计发现 **72 项**(高危 7 / 中危 26 / 低危与信息 39),**全部修复落地**,无未修复安全发现。 + +--- + +## 十七、第七轮审计(2026-08-20,HEAD `e43aeea`) + +方法:4 路并行审读(① `skills/` 全量 29 个脚本安全面——继 J12 点修后首次逐文件覆盖 ② 前端三页面深维度——DOM 注入面收敛复核 / 连接生命周期 / CDN-SRI 完整性 / 页面收敛 ③ `model_config.py` / `voice_client.py` / `client.py` 深审 ④ WS 协议面与 CLI 命令面全量逐条校验——此前按发现修,未逐命令核对)+ 主审对全部中危项逐条代码复核(Windows 开发机,无 Linux PoC)。 + +### 17.1 Batch L 修复回归验证(R1~R3 全部在位,零回归) + +| # | 核验结论 | 关键证据 | +|---|---|---| +| R1 | 在位 | `aibox.py:2586-2595` `/memos` 畸形 reminder_time try/except 按"待执行"展示 | +| R2 | 在位 | `aibox.py:2368-2384` `handle_interrupt` 仅会话级 `ai.cancel()`,全局 executor 取消已移除,注释在位 | +| R3 | 在位 | `aibox.py:2179-2196` `unregister` pop+cancel `_processing_tasks` + `websocket.close()` 兜底幂等 | + +### 17.2 新发现——中危(11 项,主审逐条代码实证) + +#### S1:`gitclient.py` GITHUB_TOKEN 嵌入 clone 命令行(凭据进 argv + 错误回显) + +`skills/gitclient.py:91-94`:`_auth_url` 把 token 拼进 `https://{TOKEN}@...` 作为 git 子进程 argv——同机 `ps` 可见;git 报错 stderr(含完整带 token URL)经 `clone()` 返回技能 stdout(`:168`)→ token 进 AI 会话日志。违反本项目 J12"凭据不进命令行"条例。**修法**:改 `git -c http.extraheader="Authorization: token ..."` 或 GIT_ASKPASS,stderr token 脱敏(~10 行)。 + +#### S2:`gitclient.py delete` 对任意路径 `shutil.rmtree`,无 WORKSPACE 边界 + +`skills/gitclient.py:271-280`:LLM 可控 `--repo_path` + `--confirm` 即递归删除任意目录(如 `~`);未校验位于 `WORKSPACE_DIR` 内、未校验是 git 仓库。**修法**:realpath 前缀匹配 WORKSPACE + 要求 `.git` 存在(~6 行)。 + +#### S4:`ssh-exec.py` SSH 密码必填且走命令行 + +`skills/ssh-exec.py:30`:`-p/--password` `required=True`——密码必进 `ps` 与 run_command 会话记录(示例还写了 `-p 12345678`)。**修法**:改环境变量/凭据文件读取(~8 行)。 + +#### S5:`sftp-transfer.py` `-p/--password` 命令行传密码 + +`skills/sftp-transfer.py:64, 86-88`:`get_password` 优先取 `args.password`,与 S4 同类。**修法**:仅保留 getpass/环境变量途径(~6 行)。 + +#### S7:`aes-crypto.py` AES 密钥经 `--key` 命令行传入 + +`skills/aes-crypto.py:452`:`--key` 十六进制密钥直进 argv → `ps` + 会话日志,直接违反 J12 条例。**修法**:仅保留 `--key-file`/`--load-key`(~5 行)。 + +#### S9:`camera.py --filename` 未校验,任意路径写文件+建目录 + +`skills/camera.py:343-344`:`os.path.join(CAMERA_DIR, args.filename)`——绝对路径使 join 失效、`../` 可逃逸;`capture_photo` 还对 dirname 无条件 `makedirs`。LLM 可控文件名 → 以 aibox 用户身份任意路径写 JPEG/任意建目录(内容不可控,主要为破坏/落点)。**修法**:basename 比对拒绝分隔符(~4 行)。 + +#### S11:`facerec.py person_name` 拼路径未校验 → 任意 .jpg 写入/删除 + +`skills/facerec.py:213, 240, 263`:`os.path.join(known_faces_dir, f"{person_name}.jpg")`——`../../tmp/x` 穿越:`add-known` 任意路径写 .jpg;`remove-known` 可删系统任意 .jpg;`get-info` 探测任意 .jpg。**修法**:person_name 白名单正则(~5 行)。 + +#### S12:`vision.py --image-path` 任意文件读取并外发第三方云 API + +`skills/vision.py:28-29, 42`:无文件类型校验,任意本地文件(`~/.cert/163mail.json`、`~/.ssh/id_rsa`)被 base64 后 POST 到 `ark.cn-beijing.volces.com`。AI 被网页/邮件内容诱导调用 → 本地敏感文件外发云端(提示注入 → 数据渗出通道)。**修法**:校验扩展名/魔数确为图片(~5 行)。 + +#### F1:清空聊天按钮必然抛出 TypeError,功能半失效(entry/frontend 双页面同源缺陷) + +`entry.html:1729-1737`、`frontend.html:1640-1651`:`.welcome-message` 在 `#messages-container` 内部(`entry.html:822-823`),`innerHTML=''` 已将其删除 → `querySelector` 返回 null → `null.cloneNode(true)` 抛 TypeError;后续 `clearAllStreamingMessages()`、计数清零(frontend)全部中断不执行。100% 可复现。**修法**:先 cloneNode 缓存再清空,两行对调(两页各 2 行)。 + +#### F2:手动断开后,已排程的自动重连定时器未取消,连接被"复活"(三页面同类) + +`entry.html:1517-1522, 1697-1701`:断线排程匿名 `setTimeout`(3s)无引用不可清除;手动断开置 `socket=null` 恰好满足重连条件 `!socket` → 倒计时到期自动重建连接,违背用户明确操作意图。`frontend.html:1428-1433, 1607-1616` 同款(闭包不判 manualDisconnect);`entry1.html:735-739, 753-761` 有 `reconnectTimer` 变量但 disconnect 未 clearTimeout。**修法**:手动断开处清除/作废定时器 + 重连闭包判 manualDisconnect(每页 2-3 行)。 + +#### M1:`voice_client.py` input 线程在 Ctrl+C 后泄漏并偷吃后续输入 + +`voice_client.py:624-626, 644-646`:`asyncio.to_thread(lambda: input(prompt))` 线程不可取消——KeyboardInterrupt 打断 await 后底层线程仍阻塞 stdin readline;循环 continue 又起新 input 线程,旧线程永久泄漏并"偷走"后续输入整行(读到无人消费即丢弃),表现为偶发丢行。`client.py:735-755` 已有现成正确方案(单守护线程 + Queue + call_soon_threadsafe)。**修法**:照搬 client.py 方案(~20 行)。 + +### 17.3 新发现——低危(26 项) + +| # | 位置 | 问题 | 修法 | +|---|---|---|---| +| S3 | `gitclient.py:162,178,195` | branch 直进 git argv 无 `--` 分隔,`--branch=--upload-pack=...` 选项注入 | ref 前插 `--` 或白名单(~4 行) | +| S6 | `sftp-transfer.py:170-172` | 递归下载远端文件名未校验,恶意服务端 `../` 路径穿越写本地 | 拒绝含 `/\`..` 的 item(~3 行) | +| S8 | `aes-crypto.py:69,331-332` | 生成密钥明文 print 进会话日志;密钥文件默认权限 | 写受保护文件 + chmod 600(~4 行) | +| S10 | `screenshot.py:156-159` | `--filename` 同类路径拼接未校验(J12 已修 PS 转义,本项仅遍历写 PNG) | 同 S9(~3 行) | +| S13 | `test-auto-appimage.sh:66` | sed 替换串未转义,文件名含 shell 元字符 → 生成的 AppRun 脚本注入 | sed 特殊字符转义/printf 生成(~4 行) | +| F3 | 三页面连接按钮 | CONNECTING 中重复点击产生孤儿连接/双后端会话 | 点击即禁按钮或先 close 非 CLOSED 旧连接(每页 1-2 行) | +| F4 | 三页面 `` CSS ×2 | font-awesome/highlight.js 样式表无 SRI(J13 只覆盖 script;CSS 劫持仅 UI 伪装,无 JS 执行) | 补 integrity+crossorigin(共 6 行) | +| F5 | 三页面消息列表 | 消息无限追加 + `data-raw-content` 双份存储,长会话内存双倍线性增长 | 超阈值头部裁剪(每页 ~5 行,可不做) | +| F6 | `frontend.html:1427` | 重连提示"N/5 秒后重试"与实际 `3000*N`ms 行为不符 | 文案修正(1 行) | +| M2 | `model_config.py` 主循环 | input 未捕获 EOFError/KeyboardInterrupt,管道耗尽/Ctrl+C 直接 traceback | 主循环包 try/except(~4 行) | +| M3 | `model_config.py:138,174` | `api` 节被改坏为非 dict 时 setdefault 后索引赋值抛 TypeError/AttributeError | isinstance 检查后重置(2-3 行) | +| M4 | `model_config.py:166-168,70,265` | `fallback` 为字符串时子串误判 + 拆成单字符数组写配置 + 展示误导 | isinstance(list) 规整(每处 1-2 行) | +| M5 | `model_config.py:75-133` | `api_key_env` 不校验合法变量名;display_name/model/api_url 不拒控制字符不限长(终端注入回显) | 正则 + isprintable + 限长(~10 行) | +| M6 | `model_config.py` 读写模式 | "启动读全量→交互数分钟→全量覆盖写",与运行中服务并发保存 lost-update(原子写不防) | 保存前 reload 重放变更(~5 行)或文档约定 | +| M7 | `voice_client.py:350-353` | 启动麦克风探测 arecord 无 timeout,设备挂死则永久卡死 | 加 timeout=5 + 捕获(2 行) | +| M8 | `client.py:799-817` | run() 正常退出后 main() 无 finally 关 websocket,服务端见异常断连 | finally disconnect(3 行) | +| W1 | `aibox.py:2388,2493,2740,2392` | WS 入口字段无类型检查,非 str 落顶层兜底"处理失败";title 传 dict → sqlite3 InterfaceError(J1 except 拦不住) | 三入口 isinstance 规整(~15-20 行) | +| W2 | `aibox.py:2394,2414` | task add `delay` 无类型/范围校验:非数字 TypeError、负数立即执行、10¹⁸ OverflowError | float 转换+钳制+友好 error(~5 行) | +| W3 | `aibox.py:2468-2469` | task complete 缺 task_id 静默无响应,与 J15 口径不一致 | else 回 error(2 行) | +| W4 | `aibox.py:2611-2612` | `/search ` 空关键词 → FTS 语法错被吞 → LIKE `%%` 兜底返回至多 100 条刷屏 | 空 query 回用法提示(2 行) | +| W5 | `aibox.py:2636-2637` 对比 `3422-3438` | `/reload` 整体重载静默丢弃 CLI 进程级覆盖(--debug/--no-scheduler 等) | reload 后重放 CLI 覆盖(~8 行)或回包注明 | +| W6 | `aibox.py:3115-3118, 3143` | 端口占用/--port 99999 等启动失败打印堆栈后**退出码 0**,守护脚本误判正常退出;--port 无范围校验 | sys.exit(1) + 1-65535 校验(~6 行) | +| W7 | `aibox.py:3437-3438, 3147` | `--scheduler-interval -1` → asyncio.sleep(负值) 空转热循环 CPU 打满;`0` 被 falsy 静默忽略 | 钳制 max(0.1, v)(2 行) | +| W8 | `aibox.py:3157,3314` | `--temperature -1/99` 无钳制直达 API 400(仅 --once 路径;失败模式安全) | 入口钳制(2 行) | +| W11 | `aibox.py:3422-3438` | config.json 的 system/scheduler 节为非 dict + CLI 覆盖 → main() 启动即 TypeError 崩 | 赋值前 isinstance 回退默认节(~6 行) | +| W12 | `aibox.py:3226-3248` | CLI `--memo-add "|x"` 空标题入库;`"a|b|"` 空 reminder_time 绕过 J1 校验 → 调度器按到期立即触发 | add_memo 标题非空 ValueError + CLI 空串规整 None(~6 行) | + +### 17.4 新发现——信息级(9 项) + +| # | 位置 | 说明 | +|---|---|---| +| S14 | `ssh-exec.py:36`、`sftp-transfer.py:94` | paramiko AutoAddPolicy 信任任意主机密钥(MITM 面)——SKILLS_FORMAT.md 已声明"仅限可信网络",实证记录 | +| S15 | `test_qemu_run.sh:86` | 缺 qemu 时自动 `sudo apt install`,无人工确认——知悉项 | +| F7 | `entry.html:930,1223`、`frontend.html:899,1144` | 模板内 `id="task-id-display"` 多次克隆产生重复 ID(功能不受影响,HTML 不规范) | +| F8 | `entry.html:1036-1039,1761-1767`、`frontend.html:998,1649` | 死代码:空 scroll 监听 / 未调用的 preserveRawContent / 恒为 0 的 toolCallCount(~15 行可清理) | +| F9 | `entry1.html:833` | 更换背景 blob URL 未 revoke(量级极小) | +| M9 | `model_config.py:371` | 提示"最长 20 秒"与实际超时 30s 不符(1 行文案) | +| M10 | `voice_client.py:51,102,351` | `/dev/null`+arecord 为 POSIX 专属,Windows 必静默禁用——若刻意(部署 Linux)可忽略,建议文件头注明平台要求 | +| W9 | `aibox.py:3067` | WS max_size 依赖库默认 1MiB,超限 1009 关连接走正常回收(R3),无应用层"消息过大"提示 | +| W10 | `aibox.py:2330-2331, 2734-2736` | 非 dict JSON 走顶层兜底;未知 `/` 命令当聊天发给 AI 有 token 费用副作用(else 分支为既有设计,改否属产品决策) | + +### 17.5 查过但无问题(摘要) + +- **skills(17 Python + 4 Shell + 8 Markdown 全量)**:163mail(凭据走 `~/.cert` 文件+SSL)、ads1115/ads1256(纯寄存器+argparse choices)、bing_search(quote 编码+无凭据)、doc2txt、hello-world、info、md2docx、nmapskills(J12 在位)、famer.sh/qing_famer.sh(变量全引用)、test_qemu_run.sh(mktemp -d);无 pickle/yaml.load/eval/exec、无可预测临时文件;**8 个 Markdown 技能零诱导越权/泄信内容** +- **前端三页面**:无 eval/new Function/document.write/insertAdjacentHTML/动态 location;DOMPurify 单参默认调用(无 ADD_TAGS/ADD_ATTR 放行);全部服务器消息汇点经净化链,三页面无漏修;全部 `