diff --git a/components/proxy/plugins/headlesslogdownload/headless_log_download.go b/components/proxy/plugins/headlesslogdownload/headless_log_download.go index 451ffad9dde659..c428fc72bdc130 100644 --- a/components/proxy/plugins/headlesslogdownload/headless_log_download.go +++ b/components/proxy/plugins/headlesslogdownload/headless_log_download.go @@ -25,7 +25,8 @@ func init() { httpcaddyfile.RegisterHandlerDirective(headlessLogDownloadModule, parseCaddyfile) } -// HeadlessLogDownload implements an HTTP handler that extracts gitpod headers +// HeadlessLogDownload implements an HTTP handler that proxies headless log downloads +// with security headers to prevent XSS attacks from malicious branch names in logs. type HeadlessLogDownload struct { Service string `json:"service,omitempty"` } @@ -93,6 +94,9 @@ func (m HeadlessLogDownload) ServeHTTP(w http.ResponseWriter, r *http.Request, n return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log")) } + setSecurityHeaders(w) + copyResponseHeaders(w, resp) + brw := newNoBufferResponseWriter(w) _, err = io.Copy(brw, resp.Body) if err != nil { @@ -103,6 +107,38 @@ func (m HeadlessLogDownload) ServeHTTP(w http.ResponseWriter, r *http.Request, n return next.ServeHTTP(w, r) } +func setSecurityHeaders(w http.ResponseWriter) { + headers := w.Header() + headers.Set("Content-Type", "text/plain; charset=utf-8") + headers.Set("X-Content-Type-Options", "nosniff") + headers.Set("X-Frame-Options", "DENY") + headers.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") + headers.Set("Referrer-Policy", "strict-origin-when-cross-origin") + headers.Set("Cache-Control", "no-cache, no-store, must-revalidate") +} + +// copyResponseHeaders copies safe headers from upstream response, excluding potentially dangerous ones +func copyResponseHeaders(w http.ResponseWriter, resp *http.Response) { + // List of safe headers to copy from upstream + safeHeaders := []string{ + "Content-Length", + "Content-Encoding", + "Content-Disposition", + "Last-Modified", + "ETag", + } + + destHeaders := w.Header() + for _, header := range safeHeaders { + if value := resp.Header.Get(header); value != "" { + destHeaders.Set(header, value) + } + } + + // Note: We intentionally do NOT copy Content-Type from upstream + // because we want to enforce text/plain for security +} + // UnmarshalCaddyfile implements Caddyfile.Unmarshaler. func (m *HeadlessLogDownload) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.Next() { @@ -172,3 +208,11 @@ func (n *noBufferWriter) Write(p []byte) (written int, err error) { return } + +func (n *noBufferWriter) Header() http.Header { + return n.w.Header() +} + +func (n *noBufferWriter) WriteHeader(statusCode int) { + n.w.WriteHeader(statusCode) +}