Files
godoxy-yusing/internal/api/v1/file/get.go
yusing 892ee95c81 fix(api/file): prevent path traversal in file API
Use os.OpenRoot to restrict file access to the application root,
preventing directory traversal attacks through the file download endpoint.

Also add test to verify path traversal attempts are blocked.
2026-03-21 10:36:45 +08:00

82 lines
2.2 KiB
Go

package fileapi
import (
"io"
"net/http"
"os"
"path"
"strings"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/common"
apitypes "github.com/yusing/goutils/apitypes"
)
type FileType string // @name FileType
const (
FileTypeConfig FileType = "config" // @name FileTypeConfig
FileTypeProvider FileType = "provider" // @name FileTypeProvider
FileTypeMiddleware FileType = "middleware" // @name FileTypeMiddleware
)
type GetFileContentRequest struct {
FileType FileType `form:"type" binding:"required,oneof=config provider middleware"`
Filename string `form:"filename" binding:"required" format:"filename"`
} // @name GetFileContentRequest
// @x-id "get"
// @BasePath /api/v1
// @Summary Get file content
// @Description Get file content
// @Tags file
// @Accept json
// @Produce application/godoxy+yaml
// @Param query query GetFileContentRequest true "Request"
// @Success 200 {string} application/godoxy+yaml "File content"
// @Failure 400 {object} apitypes.ErrorResponse
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /file/content [get]
func Get(c *gin.Context) {
var request GetFileContentRequest
if err := c.ShouldBindQuery(&request); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
f, err := os.OpenInRoot(".", request.FileType.GetPath(request.Filename))
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to open root"))
return
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to read file"))
return
}
// RFC 9512: https://www.rfc-editor.org/rfc/rfc9512.html
// xxx/yyy+yaml
c.Data(http.StatusOK, "application/godoxy+yaml", content)
}
func GetFileType(file string) FileType {
switch {
case strings.HasPrefix(path.Base(file), "config."):
return FileTypeConfig
case strings.HasPrefix(file, common.MiddlewareComposeBasePath):
return FileTypeMiddleware
}
return FileTypeProvider
}
func (t FileType) GetPath(filename string) string {
if t == FileTypeMiddleware {
return path.Join(common.MiddlewareComposeBasePath, filename)
}
return path.Join(common.ConfigBasePath, filename)
}