功能说明
下载接口常通过 Content-Disposition 告诉浏览器保存文件名,但服务端返回的值不可信:可能包含路径穿越片段、控制字符、超长名称或多种编码形式。直接把整个值拼进保存路径,可能覆盖程序文件或写到目录之外。
这个解析器优先读取 RFC 5987 的 filename* 形式,按其中的字符集解码百分号转义,再统一调用 safe_basename 只保留最后一段文件名。没有扩展形式时回退到普通 filename 参数,支持带引号和不带引号两种写法。
关键代码
两个正则分别匹配 filename* 与 filename。filename* 的值形如 UTF-8’’%E5%B9%B4%E6%8A%A5.pdf,先按单引号切出字符集和编码部分,再用指定编码严格解码;字符集不存在或字节非法时退化为默认 utf-8 的宽松解码。safe_basename 把反斜杠统一成正斜杠,取最后一段并去除首尾空白,空串、单点、双点和含空字节的名称返回 None。
使用注意
解析出的名称仍需过滤 Windows 保留名(如 con、nul)和结尾的点或空格,并按目标文件系统长度截断。允许中文文件名时不要再用 ASCII 编码保存。filename* 的字符集字段可能为空,此时按 utf-8 处理。返回 None 表示应改用默认名称,不要退化为空字符串。服务端生成响应头时同样需要对引号和换行做转义,避免响应头注入。
验证方式
四条示例分别输出 report final.pdf、passwd、年报.pdf 和 None。第二条虽然带有多个上级目录,最终只保留 basename;第三条通过 filename* 正确解码中文;第四条的双点被拒绝。可以再加入 charset 非法的用例,确认解析器回退到 utf-8 而不是抛异常。
代码示例
import re
from pathlib import PurePosixPath
from urllib.parse import unquote
NAME_STAR = re.compile(r"filename\*\s*=\s*([^;]+)", re.IGNORECASE)
NAME = re.compile(r'filename\s*=\s*(?:"([^"]*)"|([^;]+))', re.IGNORECASE)
def safe_basename(value: str) -> str | None:
candidate = PurePosixPath(value.replace("\\", "/")).name.strip()
if candidate in {"", ".", ".."} or "\x00" in candidate:
return None
return candidate
def attachment_filename(header: str) -> str | None:
match = NAME_STAR.search(header)
if match:
value = match.group(1).strip().strip('"')
charset, _, remainder = value.partition("'")
_, _, encoded = remainder.partition("'")
try:
decoded = unquote(
encoded or remainder,
encoding=charset or "utf-8",
errors="strict",
)
except (LookupError, UnicodeDecodeError):
decoded = unquote(encoded or remainder)
return safe_basename(decoded)
match = NAME.search(header)
if not match:
return None
value = match.group(1) if match.group(1) is not None else match.group(2)
return safe_basename(unquote(value.strip()))
def main() -> None:
headers = [
'attachment; filename="report final.pdf"',
"attachment; filename=../../etc/passwd",
"attachment; filename*=UTF-8''%E5%B9%B4%E6%8A%A5.pdf",
'attachment; filename=".."',
]
for header in headers:
print(attachment_filename(header))
if __name__ == "__main__":
main()