From ef652cdab1f2a88fc88053bea3cd539eb9aee647 Mon Sep 17 00:00:00 2001 From: xfy Date: Mon, 13 Apr 2026 16:40:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(static):=20=E6=B7=BB=E5=8A=A0=E7=AC=A6?= =?UTF-8?q?=E5=8F=B7=E9=93=BE=E6=8E=A5=E5=AE=89=E5=85=A8=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/config.go | 5 +++ internal/handler/static.go | 80 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 3c49f55..52ef086 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -248,6 +248,11 @@ type StaticConfig struct { // 默认为 false,内部重定向不触发中间件 // 设置为 true 时,try_files 回退会重新进入中间件链 TryFilesPass bool `yaml:"try_files_pass"` + + // SymlinkCheck 是否启用符号链接安全检查 + // 默认为 false,启用后会验证符号链接指向的文件是否在允许的路径范围内 + // 防止通过符号链接访问敏感文件(如 /etc/passwd) + SymlinkCheck bool `yaml:"symlink_check"` } // ProxyConfig 反向代理配置,支持负载均衡和健康检查。 diff --git a/internal/handler/static.go b/internal/handler/static.go index 9419334..6cce8fa 100644 --- a/internal/handler/static.go +++ b/internal/handler/static.go @@ -50,6 +50,7 @@ type StaticHandler struct { tryFiles []string useSendfile bool tryFilesPass bool + symlinkCheck bool } // NewStaticHandler 创建静态文件处理器。 @@ -190,6 +191,17 @@ func (h *StaticHandler) SetTryFiles(tryFiles []string, tryFilesPass bool, router h.router = router } +// SetSymlinkCheck 设置符号链接安全检查。 +// +// 启用后,服务文件前会验证符号链接指向的文件是否在允许的根目录范围内。 +// 防止通过符号链接访问敏感文件(如 /etc/passwd)。 +// +// 参数: +// - enabled: 是否启用符号链接安全检查 +func (h *StaticHandler) SetSymlinkCheck(enabled bool) { + h.symlinkCheck = enabled +} + // Handle 处理静态文件请求。 // // 根据请求路径查找并返回对应的静态文件。 @@ -423,6 +435,14 @@ func (h *StaticHandler) handleStandard(ctx *fasthttp.RequestCtx, reqPath string) return } + // 符号链接安全检查 + if h.symlinkCheck { + if err := h.validateSymlink(filePath); err != nil { + utils.SendError(ctx, utils.ErrForbidden) + return + } + } + // 如果是目录,尝试索引文件 if info.IsDir() { for _, idx := range h.index { @@ -504,3 +524,63 @@ func (h *StaticHandler) serveFile(ctx *fasthttp.RequestCtx, filePath string, inf ctx.Response.SetBody(data) ctx.Response.Header.SetContentType(mimeutil.DetectContentType(filePath)) } + +// validateSymlink 验证符号链接是否安全。 +// +// 检查文件是否是符号链接,如果是则验证链接指向的文件 +// 是否在允许的根目录(root 或 alias)范围内。 +// 防止通过符号链接访问敏感文件(如 /etc/passwd)。 +// +// 参数: +// - filePath: 要验证的文件路径 +// +// 返回值: +// - error: 如果符号链接不安全或解析失败,返回错误 +func (h *StaticHandler) validateSymlink(filePath string) error { + // 获取文件信息(不跟随符号链接) + info, err := os.Lstat(filePath) + if err != nil { + return err + } + + // 如果不是符号链接,直接返回成功 + if info.Mode()&os.ModeSymlink == 0 { + return nil + } + + // 获取符号链接指向的实际路径 + realPath, err := filepath.EvalSymlinks(filePath) + if err != nil { + return err + } + + // 获取允许的基础路径 + basePath := h.root + if h.alias != "" { + basePath = h.alias + } + + // 如果没有配置根目录,拒绝符号链接 + if basePath == "" { + return os.ErrPermission + } + + // 解析基础路径为绝对路径 + absBase, err := filepath.Abs(basePath) + if err != nil { + return err + } + + // 解析目标路径为绝对路径 + absTarget, err := filepath.Abs(realPath) + if err != nil { + return err + } + + // 确保目标路径在基础路径范围内 + if !strings.HasPrefix(absTarget, absBase+string(filepath.Separator)) && absTarget != absBase { + return os.ErrPermission + } + + return nil +}