fix: gosec enabled, fixed slowloris on the http.Server, no-timeout serve, webui path traversal, overly permissive perms on themes dir

This commit is contained in:
jamesread 2026-07-28 21:46:59 +01:00
parent 7c4eefa378
commit d7758a91c4
4 changed files with 96 additions and 12 deletions

View File

@ -10,6 +10,7 @@ linters:
- errcheck - errcheck
- gocritic - gocritic
- gocyclo - gocyclo
- gosec
- ineffassign - ineffassign
- misspell - misspell
- staticcheck - staticcheck
@ -18,6 +19,74 @@ linters:
settings: settings:
gocyclo: gocyclo:
min-complexity: 5 min-complexity: 5
gosec:
# Full gosec rule set (G101–G6xx), including Slowloris checks G112/G114.
enable-all-rules: true
exclusions: exclusions:
paths: paths:
- gen - gen
rules:
# Noise / fixtures in tests and local tooling.
- path: _test\.go
linters:
- gosec
- path: scripts/
linters:
- gosec
- path: cmd/
linters:
- gosec
# OliveTin's purpose is controlled command execution from config.
- path: internal/executor/
text: "G204:"
linters:
- gosec
# Operator-configured filesystem paths (entity files, touch/write helpers, persisted logs).
- path: internal/entities/
text: "G304:"
linters:
- gosec
- path: internal/filehelper/
text: "G304:"
linters:
- gosec
- path: internal/configcheck/
text: "G304:"
linters:
- gosec
- path: internal/executor/
text: "G304:"
linters:
- gosec
- path: internal/auth/otjwt/
text: "G304:"
linters:
- gosec
- path: internal/httpservers/
text: "G304:"
linters:
- gosec
# Legacy GitHub webhook HMAC-SHA1 is still a supported authType.
- path: internal/webhooks/auth\.go
text: "G505:"
linters:
- gosec
# InsecureSkipVerify is an explicit OAuth2 provider config option.
- path: internal/auth/otoauth2/
text: "G402:"
linters:
- gosec
# Secure is set dynamically from TLS / ForceSecureCookies; gosec wants a literal true.
- text: "G124:"
linters:
- gosec
# Protobuf / process exit codes mapped into int32 fields.
- text: "G115:"
linters:
- gosec

View File

@ -14,6 +14,7 @@ import (
"net/url" "net/url"
"path" "path"
"strings" "strings"
"time"
"github.com/OliveTin/OliveTin/internal/api" "github.com/OliveTin/OliveTin/internal/api"
"github.com/OliveTin/OliveTin/internal/auth" "github.com/OliveTin/OliveTin/internal/auth"
@ -155,6 +156,10 @@ func StartFrontendMux(cfg *config.Config, ex *executor.Executor) {
srv := &http.Server{ srv := &http.Server{
Addr: cfg.ListenAddressSingleHTTPFrontend, Addr: cfg.ListenAddressSingleHTTPFrontend,
Handler: securityHeadersMiddleware(cfg, mux), Handler: securityHeadersMiddleware(cfg, mux),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
// WriteTimeout intentionally unset: EventStream and StartActionAndWait need long-lived writes.
} }
log.Fatal(srv.ListenAndServe()) log.Fatal(srv.ListenAndServe())

View File

@ -2,6 +2,7 @@ package httpservers
import ( import (
"net/http" "net/http"
"time"
config "github.com/OliveTin/OliveTin/internal/config" config "github.com/OliveTin/OliveTin/internal/config"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -19,8 +20,19 @@ func StartPrometheus(cfg *config.Config) {
prometheus.Unregister(collectors.NewGoCollector()) prometheus.Unregister(collectors.NewGoCollector())
} }
http.Handle("/", promhttp.Handler()) mux := http.NewServeMux()
err := http.ListenAndServe(cfg.ListenAddressPrometheus, nil) mux.Handle("/", promhttp.Handler())
srv := &http.Server{
Addr: cfg.ListenAddressPrometheus,
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
err := srv.ListenAndServe()
if err != nil { if err != nil {
log.WithFields(log.Fields{ log.WithFields(log.Fields{

View File

@ -35,18 +35,16 @@ func NewWebUIServer(cfg *config.Config) *webUIServer {
} }
func (s *webUIServer) handleWebui(w http.ResponseWriter, r *http.Request) { func (s *webUIServer) handleWebui(w http.ResponseWriter, r *http.Request) {
// dirName := path.Dir(r.URL.Path)
// Mangle requests for any path like /logs or /config to load the webui index.html // Mangle requests for any path like /logs or /config to load the webui index.html
if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" { if path.Ext(r.URL.Path) == "" && r.URL.Path != "/" {
log.Debugf("Mangling request for %s to /index.html", r.URL.Path) log.Debugf("Mangling request for %s to /index.html", r.URL.Path)
http.ServeFile(w, r, path.Join(s.webuiDir, "index.html")) http.ServeFile(w, r, path.Join(s.webuiDir, "index.html"))
} else { return
log.Tracef("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
http.ServeFile(w, r, path.Join(s.webuiDir, r.URL.Path))
// http.StripPrefix(dirName, http.FileServer(http.Dir(s.webuiDir))).ServeHTTP(w, r)
} }
log.Tracef("Serving webui from %s for %s", s.webuiDir, r.URL.Path)
// http.Dir rejects path traversal; do not Join raw URL paths into ServeFile.
http.FileServer(http.Dir(s.webuiDir)).ServeHTTP(w, r)
} }
func (s *webUIServer) findWebuiDir() string { func (s *webUIServer) findWebuiDir() string {
@ -84,7 +82,7 @@ func (s *webUIServer) findCustomWebuiDir() string {
func (s *webUIServer) setupCustomWebuiDir() { func (s *webUIServer) setupCustomWebuiDir() {
dir := s.findCustomWebuiDir() dir := s.findCustomWebuiDir()
err := os.MkdirAll(path.Join(dir, "themes/"), 0775) err := os.MkdirAll(path.Join(dir, "themes/"), 0o750)
if err != nil { if err != nil {
log.Warnf("Could not create themes directory: %v", err) log.Warnf("Could not create themes directory: %v", err)