mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-10 10:53:36 +02:00
Finish the file API traversal fix by rooting both GET and SET operations at the
actual file-type directory instead of the process working directory. This blocks
`..` escapes from `config/` and `config/middlewares/` while preserving valid
in-root reads and writes.
Also harden the optional unauthenticated local API listener so it only starts on
loopback addresses (`localhost`, `127.0.0.1`, `::1`). This preserves same-host
automation while preventing accidental exposure on wildcard, LAN, bridge, or
public interfaces.
Add regression tests for blocked traversal on GET and SET, valid in-root writes,
and loopback-only local API address validation. Fix an unrelated config test
cleanup panic so the touched package verification can run cleanly.
Constraint: `GODOXY_LOCAL_API_ADDR` is documented for local automation and must remain usable without adding a new auth flow
Constraint: File API behavior must keep valid config/provider/middleware edits working while blocking path escapes
Rejected: Mirror the previous GET `OpenInRoot(".", ...)` approach in SET | still allows escapes from `config/` to sibling paths under the working directory
Rejected: Keep unauthenticated non-loopback local API binds and document the risk | preserves a high-severity pre-auth network exposure
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Treat `LOCAL_API_ADDR` as same-host only; if non-loopback unauthenticated access is ever needed, gate it behind a separately named explicit insecure opt-in
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/api/v1/file -run 'Test(Get|Set)_PathTraversalBlocked' -v`
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/config -run '^TestValidateLocalAPIAddr$|^TestRouteValidateInboundMTLSProfile$' -v`
Tested: `go test -count=1 -ldflags='-checklinkname=0' ./internal/api/... ./internal/config/...`
Not-tested: End-to-end runtime verification of fsnotify reload behavior after a valid in-root provider edit
91 lines
2.4 KiB
Go
91 lines
2.4 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 := request.FileType.OpenFile(request.Filename, os.O_RDONLY, 0)
|
|
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) RootPath() string {
|
|
if t == FileTypeMiddleware {
|
|
return common.MiddlewareComposeBasePath
|
|
}
|
|
return common.ConfigBasePath
|
|
}
|
|
|
|
func (t FileType) OpenFile(filename string, flag int, perm os.FileMode) (*os.File, error) {
|
|
root, err := os.OpenRoot(t.RootPath())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer root.Close()
|
|
return root.OpenFile(filename, flag, perm)
|
|
}
|