Files
liuzhuohua 7b321bfdce fix: 第二轮深度审计 —— 堵提权/注入、收口资源生命周期、低配置性能水位,版本号 1.0.5
安全:
- chpasswd 换行注入可改 root 密码:用户名/密码拒绝换行与控制字符
- 删用户保护从写死两个名字改为 UID<1000 系统账号
- WOL 接口名白名单 + 目标地址 IPv4 校验,去 sh -c 管道
- ACL hosts 改 ipaddress 严格校验;存储挂载点限定 /mnt|/media|/srv
- 文件管理器拒绝清单扩到目录级 + 按文件名拦截(authorized_keys/shadow 等)
- 前端 201 处 value="${...}" 补 esc();href 只放行 http/https;
  主题导出注释同时剥 /* 与 */

丢数据/假功能:
- ACL/文件共享/DDNS 三页切走再回来草稿被静默覆盖:新增 PAGE_DIRTY 脏页登记
- LAN 页网关/域名写进 system 死键,改绑 dnsmasq 同源
- UPnP 映射表按钮不再顺手起服务,走只读接口 read:upnpmap

资源:
- PTY 缓冲 8MB 封顶 + 会话信号量 + 后台空闲回收 + 离开页面主动断开
- Web 工作线程 48 封顶、请求体 16MB 封顶、会话 500 封顶改单调时钟、
  审计 2 万行裁剪、helpd 并发 6 封顶
- helper 日志 32MB 轮转;快照前查磁盘余量;fs 下载 512MB→20MB
  (通道单条响应 32MB,超限只会静默失败)
- 11 处配置写入改原子写;rescue/snapshot 单元补 MemoryMax(三处同步)

性能:
- /api/config 16 次开库 → 1 次 IN 查询
- cleanup 状态扫描 30s 缓存;gzip 缓存加 24MB 字节闸
- 后台标签页心跳暂停、终端轮询降频

杂项:DHCPv6 停用只留注释;租约回收用 dhcp_release(DEPS 可选登记);
VLAN 只删自建接口;公网探测清残留结果文件;快照根白名单;
心跳定时器防重复;keep_days/keep_rows 不吞 0
2026-10-02 10:25:40 +08:00

248 lines
11 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""外置存储设备(USB / Type-C / 雷电)的纯逻辑测试。
重点守住「格式化」的三重保护 —— 这是全项目最容易造成不可逆损失的操作:
1) 系统盘(承载 / /boot /boot/efi 等)禁止格式化;
2) 已挂载的设备必须先卸载;
3) 整块盘若已含分区,禁止格式化整盘(应格式化具体分区);
4) 前端二次确认必须手输设备名,后端还要再校验一次。
从 drouter-helper.py 抽取相关函数,lsblk / mkfs / mount 全部换成假的。
"""
import ast
import json
import os
import shutil
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, '..', 'backend', 'drouter-helper.py')
code = open(SRC, encoding='utf-8').read()
tree = ast.parse(code)
WANT_FN = ['_blk_sysfs', '_dev_bus', '_walk_blk', '_sysfs_model',
'_dev_is_system', '_fs_meta', '_fs_tool_ready', '_read_fstab',
'_backup_fstab', '_safe_mnt_seg', '_mnt_target_error', 'act_storage']
WANT_AS = ['FS_TYPES', '_CRITICAL_MOUNTS']
picked = []
for node in tree.body:
if isinstance(node, ast.FunctionDef) and node.name in WANT_FN:
picked.append(node)
elif isinstance(node, ast.Assign):
for t in node.targets:
if isinstance(t, ast.Name) and t.id in WANT_AS:
picked.append(node)
got = set()
for n in picked:
if isinstance(n, ast.Assign):
got.update(t.id for t in n.targets if isinstance(t, ast.Name))
else:
got.add(n.name)
missing = (set(WANT_FN) | set(WANT_AS)) - got
assert not missing, '未能从源码抽取:%s' % ', '.join(sorted(missing))
# ---- 测试替身 -------------------------------------------------------------
STATE = {'devices': [], 'sh_rc': 0, 'sh_out': '', 'sh_calls': []}
def fake_sh(cmd, timeout=None, **kw):
STATE['sh_calls'].append(cmd)
return (STATE['sh_rc'], STATE['sh_out'], '')
def fake_log(*a, **k):
pass
GEN = tempfile.mkdtemp()
def _ok(data=None, msg='操作成功', code='OK'):
return {'ok': True, 'code': code, 'msg_cn': msg, 'data': data}
def _fail(msg, code='ERR', data=None):
return {'ok': False, 'code': code, 'msg_cn': msg, 'data': data}
ns = {'os': os, 'json': json, 'shutil': shutil, 'time': __import__('time'),
're': __import__('re'),
'sh': fake_sh, 'log': fake_log, 'GEN': GEN,
'ok': _ok, 'fail': _fail,
'_storage_device_rows': lambda: STATE['devices'],
'_hbytes': lambda n: '%d B' % n}
exec(compile('\n'.join(ast.unparse(n) for n in picked), '<x>', 'exec'), ns)
_dev_bus = ns['_dev_bus']
_dev_is_system = ns['_dev_is_system']
_fs_meta = ns['_fs_meta']
_fs_tool_ready = ns['_fs_tool_ready']
act_storage = ns['act_storage']
FS_TYPES = ns['FS_TYPES']
fails = []
def ck(name, cond, extra=''):
print(' %-46s %s %s' % (name, 'OK ' if cond else 'FAIL', extra))
if not cond:
fails.append(name)
print('=' * 66)
print('一、连接方式判定(总线类型)')
ck('sysfs 含 thunderbolt → 雷电',
_dev_bus('sdb', '/sys/devices/pci0000:00/0000:00:0d.2/thunderbolt/0-1/nvme') ==
('thunderbolt', '雷电 / USB4'))
ck('sysfs 含 /usb → USB',
_dev_bus('sdc', '/sys/devices/pci0000:00/0000:00:14.0/usb2/2-1/2-1:1.0/block/sdc') ==
('usb', 'USB'))
ck('sysfs 含 nvme → NVMe',
_dev_bus('nvme0n1', '/sys/devices/pci0000:00/nvme/nvme0/nvme0n1') ==
('nvme', 'NVMe'))
ck('sysfs 含 /ata → SATA',
_dev_bus('sda', '/sys/devices/pci0000:00/ata1/host0/target0:0:0/0:0:0:0/block/sda') ==
('sata', 'SATA / SCSI'))
ck('sysfs 含 virtio → 虚拟磁盘',
_dev_bus('vda', '/sys/devices/pci0000:00/0000:00:05.0/virtio1/block/vda') ==
('virtual', '虚拟磁盘'))
ck('mmcblk 命名 → MMC/TF 卡',
_dev_bus('mmcblk0', '/sys/devices/platform/mmc0/mmcblk0') ==
('mmc', 'MMC / TF 卡'))
ck('未知路径 → unknown', _dev_bus('xxx', '/weird/path')[0] == 'unknown')
print('二、系统盘识别(禁止格式化的核心依据)')
rows = [
{'name': 'sda', 'parent': '', 'partitions': ['sda1', 'sda2'], 'mountpoint': ''},
{'name': 'sda1', 'parent': 'sda', 'partitions': [], 'mountpoint': '/boot'},
{'name': 'sda2', 'parent': 'sda', 'partitions': [], 'mountpoint': '/'},
{'name': 'sdb', 'parent': '', 'partitions': ['sdb1'], 'mountpoint': ''},
{'name': 'sdb1', 'parent': 'sdb', 'partitions': [], 'mountpoint': ''},
]
ck('sda2(挂载 /)判定为系统盘', _dev_is_system('sda2', rows)[0] is True)
ck('sda1(挂载 /boot)判定为系统盘', _dev_is_system('sda1', rows)[0] is True)
ck('整盘 sda(子分区挂 /)也判为系统盘', _dev_is_system('sda', rows)[0] is True)
ck('外接 sdb1 不是系统盘', _dev_is_system('sdb1', rows)[0] is False)
print('三、文件系统元数据')
ck('FS_TYPES 不少于 7 种', len(FS_TYPES) >= 7, '实际 %d' % len(FS_TYPES))
ck('exFAT 三平台通用',
all(_fs_meta('exfat')[k] for k in ('win', 'mac', 'linux')))
ck('ext4 仅 Linux', _fs_meta('ext4')['linux'] and not _fs_meta('ext4')['win'])
ck('FAT32 有 4GB 限制说明', '4GB' in _fs_meta('vfat')['note'])
ck('每个 fs 都有 mkfs / pkg / note',
all(f.get('mkfs') and f.get('pkg') and f.get('note') for f in FS_TYPES))
ck('不支持的 fs 返回 None', _fs_meta('reiser9') is None)
print('四、格式化工具可用性探测(PATH 里找可执行文件)')
tmpbin = tempfile.mkdtemp()
for t in ('mkfs.ext4', 'mkfs.exfat'):
p = os.path.join(tmpbin, t)
open(p, 'w').write('#!/bin/sh\n')
os.chmod(p, 0o755)
old_path = os.environ['PATH']
os.environ['PATH'] = tmpbin
try:
ck('PATH 里有 mkfs.ext4 → ready', _fs_tool_ready('ext4')[0] is True)
ck('PATH 里没有 mkfs.ntfs → not ready', _fs_tool_ready('ntfs')[0] is False)
ck('不可用时给出软件包名', 'ntfs-3g' in _fs_tool_ready('ntfs')[1])
finally:
os.environ['PATH'] = old_path
print('五、format 的三重保护(最关键)')
EXT = [{'name': 'sdb1', 'type': 'part', 'fstype': '', 'mounted': False,
'mountpoint': '', 'parts': []}]
STATE['devices'] = [
{'name': 'sda', 'type': 'disk', 'fstype': '', 'mountpoint': '', 'mounted': False,
'parent': '', 'partitions': ['sda1'], 'has_children': True},
{'name': 'sda1', 'type': 'part', 'fstype': 'ext4', 'mountpoint': '/',
'mounted': True, 'parent': 'sda', 'partitions': [], 'has_children': False},
{'name': 'sdb', 'type': 'disk', 'fstype': '', 'mountpoint': '', 'mounted': False,
'parent': '', 'partitions': ['sdb1'], 'has_children': True},
{'name': 'sdb1', 'type': 'part', 'fstype': '', 'mountpoint': '', 'mounted': False,
'parent': 'sdb', 'partitions': [], 'has_children': False},
{'name': 'sdc1', 'type': 'part', 'fstype': 'exfat', 'mountpoint': '/mnt/x',
'mounted': True, 'parent': '', 'partitions': [], 'has_children': False},
]
r = act_storage({'op': 'format', 'name': 'sda1', 'fs': 'ext4', 'confirm': 'sda1'})
ck('保护1:系统盘拒绝格式化', (not r['ok']) and r['code'] == 'SYSTEM_DISK', r.get('code'))
r = act_storage({'op': 'format', 'name': 'sdc1', 'fs': 'ext4', 'confirm': 'sdc1'})
ck('保护2:已挂载拒绝格式化', (not r['ok']) and r['code'] == 'MOUNTED', r.get('code'))
r = act_storage({'op': 'format', 'name': 'sdb', 'fs': 'ext4', 'confirm': 'sdb'})
ck('保护3:整盘含分区拒绝格式化', (not r['ok']) and r['code'] == 'DISK_HAS_PARTS',
r.get('code'))
r = act_storage({'op': 'format', 'name': 'sdb1', 'fs': 'ext4', 'confirm': 'sdb'})
ck('保护4:确认名不匹配拒绝', (not r['ok']) and r['code'] == 'NEED_CONFIRM', r.get('code'))
r = act_storage({'op': 'format', 'name': 'sdb1', 'fs': 'ufs', 'confirm': 'sdb1'})
ck('不支持的文件系统被拒', (not r['ok']) and r['code'] == 'BAD_FS', r.get('code'))
# 保持 PATH 里有 mkfs 工具,才能测到「mkfs 本身执行失败」,
# 而不是被「没装工具(NO_TOOL)」提前拦掉、掩盖了真正的分支。
os.environ['PATH'] = tmpbin
STATE['sh_calls'] = []
r = act_storage({'op': 'format', 'name': 'sdb1', 'fs': 'ext4', 'confirm': 'sdb1'})
ck('条件齐备时真正执行 mkfs(不再拦截)',
r['ok'] and STATE['sh_calls'] and STATE['sh_calls'][-1][0] == 'mkfs.ext4',
str(STATE['sh_calls'][-1] if STATE['sh_calls'] else ''))
STATE['sh_rc'] = 1
r = act_storage({'op': 'format', 'name': 'sdb1', 'fs': 'exfat', 'confirm': 'sdb1'})
ck('mkfs 执行失败时如实报错(不假装成功)',
(not r['ok']) and r['code'] == 'FORMAT', r.get('code'))
STATE['sh_rc'] = 0
os.environ['PATH'] = old_path
print('六、挂载 / 卸载')
STATE['sh_calls'] = []
r = act_storage({'op': 'mount', 'name': 'sdb1'})
ck('未格式化的设备不能挂载', (not r['ok']) and r['code'] == 'NOFS', r.get('code'))
r = act_storage({'op': 'mount', 'name': 'sdc1'})
ck('已挂载的设备不能重复挂载', (not r['ok']) and r['code'] == 'ALREADY', r.get('code'))
r = act_storage({'op': 'umount', 'name': 'sdb1'})
ck('未挂载的设备不能卸载', (not r['ok']) and r['code'] == 'NOTMOUNTED', r.get('code'))
STATE['sh_rc'] = 1
r = act_storage({'op': 'umount', 'name': 'sdc1'})
ck('卸载失败时给出排查命令(lsof/fuser)',
(not r['ok']) and ('lsof' in r['msg_cn']), r.get('code'))
STATE['sh_rc'] = 0
print('七、fstab 开机自动挂载')
# 不测真实写入(那会动 /etc/fstab),只测参数校验与重复检测:
# 这两条最容易出「写进去了但开机起不来」和「同一设备被重复追加两次」。
r = act_storage({'op': 'fstab_add', 'name': 'sda', 'target': '/mnt/x'})
ck('无 UUID 的设备不能写 fstab',
r.get('code') in ('NOUUID', 'NODEV'), r.get('code'))
r = act_storage({'op': 'fstab_del', 'uuid': ''})
ck('fstab_del 缺少 UUID 时报错', r.get('code') == 'NOUUID', r.get('code'))
# 重复检测:把 /etc/fstab 内容里塞进同一 UUID,应当判定为已存在
ns['_read_fstab'] = lambda: '# test\nUUID=AAAA-1111 /mnt/usb exfat defaults 0 0\n'
for d in STATE['devices']:
if d['name'] == 'sdb1':
d['uuid'] = 'AAAA-1111'
d['fstype'] = 'exfat'
r = act_storage({'op': 'fstab_add', 'name': 'sdb1', 'target': '/mnt/usb'})
ck('同一 UUID 重复添加会被拒绝', r.get('code') == 'DUP', r.get('code'))
print('八、前端必须有的要素')
js = open(os.path.join(HERE, '..', 'web', 'app.js'), encoding='utf-8').read()
for k, label in [('stgLoad', '读取设备列表'), ('stgFormat', '格式化弹窗'),
('stg-confirm', '手输设备名确认'), ('stgShare', '一键加入共享'),
('开机挂载', '开机自动挂载按钮'), ('外置存储设备', '卡片标题')]:
ck('前端含 %s' % label, k in js)
ck('格式化弹窗会校验确认名再提交',
'confirm !== d.name' in js)
ck('已挂载时格式化按钮禁用', "d.mounted ? 'disabled" in js or 'disabled title="请先卸载再格式化"' in js)
print('=' * 66)
print('结果:失败 %d 项' % len(fails))
shutil.rmtree(tmpbin, ignore_errors=True)
shutil.rmtree(GEN, ignore_errors=True)
sys.exit(1 if fails else 0)