mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 10:10:49 +00:00
a7f5acc1e3
- execMapUpdate: validate column names with strict regex to prevent SQL injection - HTTP update handlers: add field allowlists (agents, providers, custom_tools, mcp, channel_instances) - pqStringArray: properly escape array elements to prevent PostgreSQL array literal injection - scanStringArray: handle quoted elements in PostgreSQL array format - MCP bridge: wrap tool results as external/untrusted content to prevent prompt injection - File serving: block access to sensitive system directories (/etc, /proc, /sys, etc.) - Sandbox: fail closed when Docker unavailable instead of silent fallback to host - Shell deny: fix base64 --decode bypass, add host exec 1MB output limit - ILIKE queries: escape % and _ wildcards in knowledge_graph, custom_tools, channel_instances Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
95 lines
2.5 KiB
Go
95 lines
2.5 KiB
Go
package http
|
|
|
|
import (
|
|
"log/slog"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
)
|
|
|
|
// FilesHandler serves files over HTTP with Bearer token auth.
|
|
// Accepts absolute paths — the auth token protects against unauthorized access.
|
|
type FilesHandler struct {
|
|
token string
|
|
}
|
|
|
|
// NewFilesHandler creates a handler that serves files by absolute path.
|
|
func NewFilesHandler(token string) *FilesHandler {
|
|
return &FilesHandler{token: token}
|
|
}
|
|
|
|
// RegisterRoutes registers the file serving route.
|
|
func (h *FilesHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /v1/files/{path...}", h.auth(h.handleServe))
|
|
}
|
|
|
|
func (h *FilesHandler) auth(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Accept token via Bearer header or ?token= query param (for <img src>).
|
|
provided := extractBearerToken(r)
|
|
if provided == "" {
|
|
provided = r.URL.Query().Get("token")
|
|
}
|
|
if !requireAuthBearer(h.token, "", provided, w, r) {
|
|
return
|
|
}
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
// deniedFilePrefixes blocks access to sensitive system directories.
|
|
// Defense-in-depth: the auth token is the primary barrier, but restricting
|
|
// known-sensitive paths limits damage if a token leaks.
|
|
var deniedFilePrefixes = []string{
|
|
"/etc/", "/proc/", "/sys/", "/dev/",
|
|
"/root/", "/boot/", "/run/",
|
|
"/var/run/", "/var/log/",
|
|
}
|
|
|
|
func (h *FilesHandler) handleServe(w http.ResponseWriter, r *http.Request) {
|
|
locale := extractLocale(r)
|
|
urlPath := r.PathValue("path")
|
|
if urlPath == "" {
|
|
http.Error(w, i18n.T(locale, i18n.MsgRequired, "path"), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Prevent path traversal
|
|
if strings.Contains(urlPath, "..") {
|
|
slog.Warn("security.files_traversal", "path", urlPath)
|
|
http.Error(w, i18n.T(locale, i18n.MsgInvalidPath), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// URL path is the absolute path with leading "/" stripped (e.g. "app/.goclaw/workspace/file.png")
|
|
absPath := filepath.Clean("/" + urlPath)
|
|
|
|
// Block access to sensitive system directories
|
|
for _, prefix := range deniedFilePrefixes {
|
|
if strings.HasPrefix(absPath, prefix) {
|
|
slog.Warn("security.files_denied_path", "path", absPath)
|
|
http.Error(w, i18n.T(locale, i18n.MsgInvalidPath), http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
info, err := os.Stat(absPath)
|
|
if err != nil || info.IsDir() {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
// Set Content-Type from extension
|
|
ext := filepath.Ext(absPath)
|
|
ct := mime.TypeByExtension(ext)
|
|
if ct != "" {
|
|
w.Header().Set("Content-Type", ct)
|
|
}
|
|
|
|
http.ServeFile(w, r, absPath)
|
|
}
|