下载入口只接受一个文件名,看起来很简单,却容易把“用户想下载什么”和“服务器允许公开什么”混在一起。如果直接把输入拼到磁盘目录后面,程序就需要面对路径跳转、绝对路径和链接文件等边界问题。

Security Review Skill:本地下载资源的编号与路径边界审查方法示意图
增强编程助手 · 原创方法示意图

这篇用ECC中的Security Review Skill审查一个完全本地的演示:下载者只提交资源编号,服务器根据固定目录表选择公开文件。最终得到输入边界说明、一个可运行的路径选择函数和测试记录。不启动公网服务,不扫描第三方网站,也不把练习结果当作生产安全认证。

Skill说明、安装入口和许可按ECC已有资源包对应提交核对。下文Python函数是本站编写的示例,已在本地做边界测试;建议提示词没有冒充在读者工具里真实调用成功。

一、定位正确的Skill与资源包

官方仓库为affaan-m/ECC,目标目录是skills/security-review。本文使用已有ECC整包对应的提交d2ef2680594f9a17526a6e1a2e18b3d242c5d17b,页面版本沿用2026-11-07源码快照,不声称是最新发布。

资源表下载的是ECC整包,并非单独新制作的安全审查工具。保留README和仓库MIT许可证,在包内定位skills/security-review/SKILL.md,阅读输入验证、白名单、授权和错误信息等检查点。不执行包内安装脚本或第三方示例代码来完成本文练习。

Security Review提供的是审查指引,覆盖输入、密钥、鉴权和依赖等主题。它不是能够证明任何网站“已经没有漏洞”的扫描器,也不会替你自动确认业务授权规则。

二、选择一种安装方式并确认加载

使用Claude Code插件渠道时,官方README给出的原生入口为:

/plugin marketplace add https://github.com/affaan-m/ECC
/plugin install ecc@ecc

这些命令在Claude Code内执行,不是PowerShell命令。已安装ECC的读者先检查插件状态,不要在完整插件安装之上重复叠加一套手工安装。官方README明确区分了安装方式与可能的冲突处理。

插件渠道安装的具体版本可能不同于本站旧快照;使用前检查自己环境实际加载的Skill内容。若只研究下载源码,可先读取目标Markdown,记录为人工按清单审查,不把阅读文件称作插件已经启用。

新开会话后确认开发助手能读取security-review技能,再把下面的范围约定与示例代码交给它。不要上传真实服务器路径、密钥或客户文件;本例只需要一个临时目录和无敏感信息的文本。

三、把下载权限与文件位置分开

假设只有一份公开资料,资源编号为guide,磁盘文件名为guide.txt。编号到文件的映射由维护者在服务端配置,用户不能提交任意磁盘路径,也不能修改目录表。

本篇只处理“资源编号怎样定位到受控文件”这一层。若真实文件仅对登录用户开放,还必须先做用户身份与资源授权检查;知道一个合法编号,不等于有权下载它。公开目录表也不能成为绕过账号权限的理由。

目录必须由可信维护者管理,下载进程不应允许普通用户写入或替换其中的文件。下面函数没有解决并发替换、完整HTTP响应头、下载频率或访问审计;这些属于接入真实服务时另行设计与验证的内容。

四、先让助手说明数据流与假设

把范围和代码发送给支持该Skill的开发助手:

请使用security-review检查这个本地资源定位函数。
仅审查我提供的演示目录与代码,不访问第三方站点,不启动服务器。
先画出文字数据流:不可信资源编号、服务端目录表、受控根目录、目标文件。
分别说明输入验证、路径边界、文件类型与授权仍缺什么。
每条发现附对应代码或需求依据,不把未验证风险写成已复现漏洞。
先给审查记录,等我确认后再修改。

这一步应得到清楚的边界,而不是一句“已做安全处理”。例如,编号来自调用者,目录表由程序控制,根目录来自可信部署配置;目标文件仍需检查是否存在、是否普通文件,以及是否离开公开范围。

OWASP关于路径遍历的资料建议尽量避免把用户输入直接用作文件路径,并通过已知允许值等方式约束访问。本例选择资源编号目录表,是为了缩小输入含义,不依赖反复删除某几个危险字符。

五、保存一个只选择路径的演示函数

准备Python 3.10或更高版本,将以下内容保存为download_lookup.py。程序不启动HTTP接口,也不在读取用户输入后直接发送文件。

from pathlib import Path


FILES = {"guide": "guide.txt"}


def locate(root, resource_id):
    if not isinstance(resource_id, str) or resource_id not in FILES:
        raise ValueError("unknown resource")
    base = Path(root).resolve(strict=True)
    if not base.is_dir():
        raise NotADirectoryError("public root is not a directory")
    candidate = base / FILES[resource_id]
    if candidate.is_symlink():
        raise PermissionError("symbolic links are not allowed")
    target = candidate.resolve(strict=True)
    if not target.is_relative_to(base):
        raise PermissionError("outside public root")
    if not target.is_file():
        raise FileNotFoundError("resource is not a regular file")
    return target

本例只允许精确编号guide,不会把Guide自动小写,也不去掉输入空格。这样的严格规则便于核查;真实项目若允许别名,应先规定映射,再补相应测试,不能在多个入口各自猜测。

is_relative_to检查的是解析后路径的目录关系,不是字符串前缀。比如public-backup与public名字开头相同,并不说明前者属于后者。不要用字符串startswith替代目录层级判断。

函数还显式拒绝目标符号链接。但这并不等于跨平台的所有文件系统特殊机制都已覆盖;目录权限和可信文件管理仍是前提,不能只靠这几行代码保护可被攻击者随意改写的目录。

六、在临时目录运行正常与异常用例

保存下面内容为demo_lookup.py,与download_lookup.py放在同一目录。程序只在自动创建的临时目录里写入示例文本,退出后由临时目录机制清理,不操作真实网站文件。

from pathlib import Path
from tempfile import TemporaryDirectory
from download_lookup import locate


with TemporaryDirectory() as directory:
    root = Path(directory)
    (root / "guide.txt").write_text("Public demo only", encoding="utf-8")
    print(locate(root, "guide").name)
    for value in ("../private.txt", "/private.txt", "guide.txt", "Guide", ""):
        try:
            locate(root, value)
        except ValueError:
            print("rejected:", repr(value))
        else:
            raise AssertionError("unexpected accepted identifier")

运行python demo_lookup.py,第一行应是guide.txt,后面五条输入都应被拒绝。这些字符串仅用于你自己创建的本地练习,不用于向任何真实站点探测。

继续在测试副本里检查:文件缺失应失败;同名位置变成目录应失败;根目录不存在应失败;输入不是字符串应失败。需要检查符号链接时,只在支持且有权限创建链接的临时环境进行;无法创建时记录未测试,不宣称该环境已经通过这一项。

七、审查结论应写到什么程度

合理结论可以是:“本地示例只接受固定资源编号;正常文件可定位,未列入目录的输入被拒绝;文件缺失和目录类型的失败符合预期。”这比写“路径遍历漏洞彻底修复”更准确,因为本文没有审查真实路由、框架解码和部署权限。

还要明确列出未覆盖项:调用前是否认证,是否检查该用户能访问该资源,返回错误是否暴露内部路径,以及文件选择后到真正打开前是否可能被替换。这些不能因为Skill输出一张完整清单就自动打勾。

接入框架后,由应用捕获内部异常,向外返回适合业务的通用失败信息。不要把异常堆栈、服务器绝对路径和配置内容直接返回给下载者。内部日志是否记录具体路径,也应按管理范围和排错需要决定。

八、从本地练习走向可交付检查

保留输入规则、目录表、受控目录权限说明、测试输出和剩余问题。需要新增第二份资源时,先由维护者更新目录表和文件,再补正常编号、未知编号与文件状态检查,而不是开放“任意文件名”省去配置。

若公开文件会频繁被替换,应采用适合部署环境的安全文件打开和发布方式,处理检查与使用之间的竞态;本例返回Path对象,不能单独解决这个问题。不要直接把演示函数包装成公网接口就认为已经完成上线审计。

本篇完成的是一项有边界的审查练习:知道输入从哪里来、允许访问哪些文件、证据支持什么结论,以及哪些条件还没有检查。后续再扩展身份、授权与下载服务时,沿着这些边界逐项验证。

九、出现失败时按证据定位

合法编号也失败时,先检查配置的根目录是否存在,再检查guide.txt是否确实放在该目录。不要为了让测试通过,把根目录改成整个磁盘或网站上级目录;应该修正演示文件与部署配置之间的对应关系。

如果未知编号被接受,先确认调用的是本文函数,而不是另一个仍然接收文件名的旧入口。真实应用可能有多个下载路由,仅修复其中一处并不能说明所有入口都采用同样规则。审查时列出入口与调用链,再分别确认实际使用的选择逻辑。

如果符号链接测试无法运行,记录操作系统、权限限制与未完成项即可。不要改动系统安全设置只为凑齐测试数量;在具备正常测试权限的隔离环境补做这一项,比把失败日志改成通过更有价值。

后续交付报告可以分为已验证、依据配置确认、尚待验证三种状态,每项附证据位置。尤其是授权和并发文件替换问题,应明确留给接入阶段处理,而不是在本地路径函数测试通过后一起标为完成。

官方资料