60 lines
2.4 KiB
Python
60 lines
2.4 KiB
Python
"""
|
||
安全辅助函数(评审 H5 / 上传嗅探 / 路径校验)。
|
||
- safe_traceback: 仅在 DEBUG 时把堆栈返回客户端,生产环境只记到服务端日志
|
||
- validate_id: 校验 URL 传入的 id,避免落入文件系统 sink(路径穿越/异常字符)
|
||
- upload_content_ok: 按魔术字节嗅探上传内容,防止改扩展名的二进制混入
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
import traceback as _traceback
|
||
|
||
from .logger import get_logger
|
||
|
||
# 允许的 id 字符集:字母数字 + 下划线 + 连字符,长度 1-64。
|
||
# 排除 '/'、'.'、'\\' 等可用于路径穿越或越级的字符。
|
||
_ID_RE = re.compile(r'^[A-Za-z0-9_-]{1,64}$')
|
||
|
||
|
||
def safe_traceback() -> str:
|
||
"""
|
||
H5:完整堆栈始终写入服务端日志;仅在 DEBUG 模式才把堆栈返回客户端,
|
||
生产环境返回通用提示,避免向客户端泄露内部路径/栈帧。
|
||
"""
|
||
# 延迟导入 Config,避免与配置模块的潜在循环依赖
|
||
from ..config import Config
|
||
tb = _traceback.format_exc()
|
||
get_logger('mirofish.error').error(tb)
|
||
return tb if Config.DEBUG else 'Internal server error (see server logs)'
|
||
|
||
|
||
def validate_id(value: str, kind: str = 'id') -> str:
|
||
"""
|
||
路径校验:拒绝任何不匹配 _ID_RE 的 id(含 '..'、'/'、空值),
|
||
在 id 进入 os.path.join / makedirs / rmtree 之前阻断路径穿越。
|
||
"""
|
||
if not isinstance(value, str) or not _ID_RE.match(value):
|
||
raise ValueError(f'Invalid {kind}: {value!r}')
|
||
return value
|
||
|
||
|
||
def upload_content_ok(file_storage, filename: str) -> bool:
|
||
"""
|
||
上传嗅探:按扩展名校验文件头部内容,使改名的二进制无法通过扩展名白名单。
|
||
- pdf:必须以 %PDF- 开头
|
||
- txt/md/markdown:头部不得含 NUL 字节(典型二进制特征)
|
||
读取后将流指针复位,避免影响后续保存。
|
||
"""
|
||
ext = os.path.splitext(filename)[1].lower().lstrip('.')
|
||
head = file_storage.read(512)
|
||
file_storage.seek(0)
|
||
if ext == 'pdf':
|
||
return head[:5] == b'%PDF-'
|
||
if ext in ('txt', 'md', 'markdown'):
|
||
# 接受带 BOM 的 UTF-16/UTF-32/UTF-8 文本(这些合法文本会含 NUL 字节,
|
||
# 与 file_parser 的多编码支持一致);否则按头部含 NUL 判定为二进制并拒绝。
|
||
if head.startswith((b'\xff\xfe', b'\xfe\xff', b'\xef\xbb\xbf')):
|
||
return True
|
||
return b'\x00' not in head
|
||
return False
|