344 lines
8.6 KiB
V
344 lines
8.6 KiB
V
module hono
|
|
|
|
import os
|
|
|
|
// 文件类型白名单
|
|
const allowed_file_extensions = [
|
|
// 文档类型
|
|
'.txt', '.md', '.pdf', '.doc', '.docx',
|
|
// 图片类型
|
|
'.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.webp',
|
|
// 音视频类型
|
|
'.mp3', '.mp4', '.wav', '.avi', '.mov', '.webm',
|
|
// 压缩文件
|
|
'.zip', '.rar', '.7z', '.tar', '.gz',
|
|
// Web 文件
|
|
'.html', '.htm', '.css', '.js', '.json', '.xml',
|
|
// 字体文件
|
|
'.ttf', '.otf', '.woff', '.woff2', '.eot'
|
|
]
|
|
|
|
// 危险字符列表
|
|
const dangerous_chars = ['<', '>', '"', '|', '?', '*', '\x00', '\r', '\n']
|
|
|
|
// 危险路径模式
|
|
const dangerous_patterns = [
|
|
'..', // 路径遍历
|
|
'~', // 用户目录
|
|
'$', // 环境变量
|
|
'%', // Windows 环境变量
|
|
'\\\\', // UNC 路径
|
|
'/..', // Unix 路径遍历
|
|
'\\..', // Windows 路径遍历
|
|
'../', // 相对路径遍历
|
|
'..\\', // Windows 相对路径遍历
|
|
]
|
|
|
|
// 安全路径验证选项
|
|
pub struct PathValidationOptions {
|
|
pub:
|
|
allow_absolute_paths bool // 是否允许绝对路径
|
|
allow_hidden_files bool // 是否允许隐藏文件(以.开头)
|
|
check_file_extension bool = true // 是否检查文件扩展名白名单
|
|
max_path_length int = 260 // 最大路径长度(Windows限制)
|
|
allowed_base_paths []string // 允许的基础路径列表
|
|
custom_allowed_extensions []string // 自定义允许的扩展名
|
|
}
|
|
|
|
// 默认路径验证选项
|
|
pub fn default_path_validation_options() PathValidationOptions {
|
|
return PathValidationOptions{}
|
|
}
|
|
|
|
// 增强的路径安全检查
|
|
pub fn validate_file_path(path string, options PathValidationOptions) !string {
|
|
// 1. 基本检查
|
|
if path.len == 0 {
|
|
return error('Empty path not allowed')
|
|
}
|
|
|
|
if path.len > options.max_path_length {
|
|
return error('Path too long: ${path.len} > ${options.max_path_length}')
|
|
}
|
|
|
|
// 2. 规范化路径
|
|
clean_path := normalize_path(path)
|
|
|
|
// 3. 检查危险字符
|
|
for dangerous_char in dangerous_chars {
|
|
if clean_path.contains(dangerous_char) {
|
|
return error('Dangerous character detected: ${dangerous_char}')
|
|
}
|
|
}
|
|
|
|
// 4. 检查危险模式
|
|
for pattern in dangerous_patterns {
|
|
if clean_path.contains(pattern) {
|
|
return error('Dangerous path pattern detected: ${pattern}')
|
|
}
|
|
}
|
|
|
|
// 5. 检查绝对路径
|
|
if !options.allow_absolute_paths {
|
|
if is_absolute_path(clean_path) {
|
|
return error('Absolute paths not allowed')
|
|
}
|
|
}
|
|
|
|
// 6. 检查隐藏文件
|
|
if !options.allow_hidden_files {
|
|
if is_hidden_file(clean_path) {
|
|
return error('Hidden files not allowed')
|
|
}
|
|
}
|
|
|
|
// 7. 检查文件扩展名
|
|
if options.check_file_extension {
|
|
if !is_allowed_file_type(clean_path, options.custom_allowed_extensions) {
|
|
return error('File type not allowed')
|
|
}
|
|
}
|
|
|
|
// 8. 检查基础路径限制
|
|
if options.allowed_base_paths.len > 0 {
|
|
if !is_within_allowed_paths(clean_path, options.allowed_base_paths) {
|
|
return error('Path outside allowed directories')
|
|
}
|
|
}
|
|
|
|
return clean_path
|
|
}
|
|
|
|
// 路径规范化
|
|
fn normalize_path(path string) string {
|
|
// 移除多余的空白字符
|
|
mut clean_path := path.trim_space()
|
|
|
|
// 统一路径分隔符为 /
|
|
clean_path = clean_path.replace('\\', '/')
|
|
|
|
// 移除重复的路径分隔符
|
|
for clean_path.contains('//') {
|
|
clean_path = clean_path.replace('//', '/')
|
|
}
|
|
|
|
// 移除末尾的路径分隔符
|
|
if clean_path.ends_with('/') && clean_path.len > 1 {
|
|
clean_path = clean_path[..clean_path.len - 1]
|
|
}
|
|
|
|
return clean_path
|
|
}
|
|
|
|
// 检查是否为绝对路径
|
|
fn is_absolute_path(path string) bool {
|
|
// Unix/Linux 绝对路径
|
|
if path.starts_with('/') {
|
|
return true
|
|
}
|
|
|
|
// Windows 绝对路径 (C:, D: 等)
|
|
if path.len >= 2 && path[1] == `:` {
|
|
return true
|
|
}
|
|
|
|
// UNC 路径 (\\server\share)
|
|
if path.starts_with('//') {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// 检查是否为隐藏文件
|
|
fn is_hidden_file(path string) bool {
|
|
// 获取文件名部分
|
|
parts := path.split('/')
|
|
if parts.len == 0 {
|
|
return false
|
|
}
|
|
|
|
filename := parts.last()
|
|
return filename.starts_with('.') && filename != '.' && filename != '..'
|
|
}
|
|
|
|
// 检查文件类型是否被允许
|
|
fn is_allowed_file_type(path string, custom_extensions []string) bool {
|
|
ext := os.file_ext(path).to_lower()
|
|
|
|
// 如果没有扩展名,拒绝
|
|
if ext == '' {
|
|
return false
|
|
}
|
|
|
|
// 检查自定义扩展名列表
|
|
if custom_extensions.len > 0 {
|
|
return ext in custom_extensions
|
|
}
|
|
|
|
// 检查默认白名单
|
|
return ext in allowed_file_extensions
|
|
}
|
|
|
|
// 检查路径是否在允许的基础路径内
|
|
fn is_within_allowed_paths(path string, allowed_paths []string) bool {
|
|
for allowed_path in allowed_paths {
|
|
normalized_allowed := normalize_path(allowed_path)
|
|
if path.starts_with(normalized_allowed) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 便捷函数:基本路径安全检查(向后兼容)
|
|
pub fn is_safe_path(path string) bool {
|
|
result := validate_file_path(path, default_path_validation_options()) or { return false }
|
|
return result.len > 0
|
|
}
|
|
|
|
// 便捷函数:文件路径安全检查(向后兼容)
|
|
pub fn is_safe_file_path(path string) bool {
|
|
return is_safe_path(path)
|
|
}
|
|
|
|
// 输入验证:文件哈希
|
|
pub fn validate_file_hash(hash string) !string {
|
|
if hash.len == 0 {
|
|
return error('File hash cannot be empty')
|
|
}
|
|
|
|
// MD5 哈希长度检查
|
|
if hash.len != 32 {
|
|
return error('Invalid hash length: expected 32, got ${hash.len}')
|
|
}
|
|
|
|
// 检查是否只包含十六进制字符
|
|
for ch in hash {
|
|
if !ch.is_hex_digit() {
|
|
return error('Invalid hash character: ${ch}')
|
|
}
|
|
}
|
|
|
|
return hash.to_lower()
|
|
}
|
|
|
|
// 输入验证:分片索引
|
|
pub fn validate_chunk_index(index_str string, max_chunks int) !int {
|
|
if index_str.len == 0 {
|
|
return error('Chunk index cannot be empty')
|
|
}
|
|
|
|
// 检查是否只包含数字
|
|
for ch in index_str {
|
|
if !ch.is_digit() {
|
|
return error('Chunk index must be a number')
|
|
}
|
|
}
|
|
|
|
index := index_str.int()
|
|
|
|
if index < 0 {
|
|
return error('Chunk index cannot be negative')
|
|
}
|
|
|
|
if max_chunks > 0 && index >= max_chunks {
|
|
return error('Chunk index ${index} exceeds maximum ${max_chunks}')
|
|
}
|
|
|
|
return index
|
|
}
|
|
|
|
// 输入验证:文件大小
|
|
pub fn validate_file_size(size_str string, max_size int) !int {
|
|
if size_str.len == 0 {
|
|
return error('File size cannot be empty')
|
|
}
|
|
|
|
// 检查是否只包含数字
|
|
for ch in size_str {
|
|
if !ch.is_digit() {
|
|
return error('File size must be a number')
|
|
}
|
|
}
|
|
|
|
size := size_str.int()
|
|
|
|
if size <= 0 {
|
|
return error('File size must be positive')
|
|
}
|
|
|
|
if size > max_size {
|
|
return error('File size ${size} exceeds maximum ${max_size}')
|
|
}
|
|
|
|
return size
|
|
}
|
|
|
|
// 输入验证:文件名
|
|
pub fn validate_filename(filename string) !string {
|
|
if filename.len == 0 {
|
|
return error('Filename cannot be empty')
|
|
}
|
|
|
|
if filename.len > 255 {
|
|
return error('Filename too long: ${filename.len} > 255')
|
|
}
|
|
|
|
// 检查危险字符
|
|
for dangerous_char in dangerous_chars {
|
|
if filename.contains(dangerous_char) {
|
|
return error('Dangerous character in filename: ${dangerous_char}')
|
|
}
|
|
}
|
|
|
|
// 检查保留名称 (Windows)
|
|
reserved_names := ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
|
|
'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
|
|
'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9']
|
|
|
|
name_without_ext := filename.split('.')[0].to_upper()
|
|
if name_without_ext in reserved_names {
|
|
return error('Reserved filename not allowed: ${filename}')
|
|
}
|
|
|
|
return filename
|
|
}
|
|
|
|
// 安全的内容类型检测
|
|
pub fn get_safe_content_type(file_path string) string {
|
|
ext := os.file_ext(file_path).to_lower()
|
|
|
|
// 只返回已知安全的内容类型
|
|
match ext {
|
|
'.html', '.htm' { return 'text/html; charset=utf-8' }
|
|
'.css' { return 'text/css; charset=utf-8' }
|
|
'.js' { return 'application/javascript; charset=utf-8' }
|
|
'.json' { return 'application/json; charset=utf-8' }
|
|
'.xml' { return 'application/xml; charset=utf-8' }
|
|
'.txt' { return 'text/plain; charset=utf-8' }
|
|
'.md' { return 'text/markdown; charset=utf-8' }
|
|
'.pdf' { return 'application/pdf' }
|
|
'.png' { return 'image/png' }
|
|
'.jpg', '.jpeg' { return 'image/jpeg' }
|
|
'.gif' { return 'image/gif' }
|
|
'.svg' { return 'image/svg+xml' }
|
|
'.ico' { return 'image/x-icon' }
|
|
'.woff' { return 'font/woff' }
|
|
'.woff2' { return 'font/woff2' }
|
|
'.ttf' { return 'font/ttf' }
|
|
'.eot' { return 'application/vnd.ms-fontobject' }
|
|
'.otf' { return 'font/otf' }
|
|
'.mp4' { return 'video/mp4' }
|
|
'.webm' { return 'video/webm' }
|
|
'.mp3' { return 'audio/mpeg' }
|
|
'.wav' { return 'audio/wav' }
|
|
'.zip' { return 'application/zip' }
|
|
'.tar' { return 'application/x-tar' }
|
|
'.gz' { return 'application/gzip' }
|
|
else {
|
|
// 对于未知类型,返回安全的默认类型
|
|
return 'application/octet-stream'
|
|
}
|
|
}
|
|
} |