iQQBot 7137b3a8ba
[proxy] add security headers (#20970)
Co-authored-by: Ona <no-reply@ona.com>
2025-07-23 07:44:34 -04:00

219 lines
6.2 KiB
Go

// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.
package workspacedownload
import (
"fmt"
"io"
"net/http"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
const (
headlessLogDownloadModule = "gitpod.headless_log_download"
)
func init() {
caddy.RegisterModule(HeadlessLogDownload{})
httpcaddyfile.RegisterHandlerDirective(headlessLogDownloadModule, parseCaddyfile)
}
// 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"`
}
// CaddyModule returns the Caddy module information.
func (HeadlessLogDownload) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.gitpod_headless_log_download",
New: func() caddy.Module { return new(HeadlessLogDownload) },
}
}
// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (m HeadlessLogDownload) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
query := r.URL.RawQuery
if query != "" {
query = "?" + query
}
// server has an endpoint on the same path that returns the upstream endpoint for the actual download
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
u := fmt.Sprintf("%v%v%v", m.Service, origReq.URL.Path, query)
client := http.Client{Timeout: 5 * time.Second}
req, err := http.NewRequest("GET", u, nil)
if err != nil {
caddy.Log().Sugar().Errorf("cannot resolve headless log URL %v: %w", u, err)
return fmt.Errorf("server error: cannot resolve headless log URL")
}
// pass browser headers
// TODO (aledbf): check if it's possible to narrow the list
for k, vv := range r.Header {
for _, v := range vv {
req.Header.Add(k, v)
}
}
// query server and parse response
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("server error: cannot resolve headless log URL")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Bad Request: /headless-log-download/get returned with code %v", resp.StatusCode)
}
upstreamURLBytes, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("server error: cannot obtain headless log redirect URL")
}
upstreamURL := string(upstreamURLBytes)
// perform the upstream request here
resp, err = http.Get(upstreamURL)
if err != nil {
caddy.Log().Sugar().Errorf("error starting download of prebuild log for %v: %v", upstreamURL, err)
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
caddy.Log().Sugar().Errorf("invalid status code downloading prebuild log for %v: %v", upstreamURL, resp.StatusCode)
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 {
caddy.Log().Sugar().Errorf("error proxying prebuild log download for %v: %v", upstreamURL, err)
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("unexpected error downloading prebuild log"))
}
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() {
return d.Err("expected token following filter")
}
for d.NextBlock(0) {
key := d.Val()
var value string
d.Args(&value)
if d.NextArg() {
return d.ArgErr()
}
switch key {
case "service":
m.Service = value
default:
return d.Errf("unrecognized subdirective '%s'", d.Val())
}
}
if m.Service == "" {
return fmt.Errorf("Please configure the service subdirective")
}
return nil
}
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
m := new(HeadlessLogDownload)
err := m.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}
return m, nil
}
// Interface guards
var (
_ caddyhttp.MiddlewareHandler = (*HeadlessLogDownload)(nil)
_ caddyfile.Unmarshaler = (*HeadlessLogDownload)(nil)
)
// noBufferWriter ResponseWriter that allow an HTTP handler to flush buffered data to the client.
type noBufferWriter struct {
w http.ResponseWriter
flusher http.Flusher
}
func newNoBufferResponseWriter(w http.ResponseWriter) *noBufferWriter {
writer := &noBufferWriter{
w: w,
}
if flusher, ok := w.(http.Flusher); ok {
writer.flusher = flusher
}
return writer
}
func (n *noBufferWriter) Write(p []byte) (written int, err error) {
written, err = n.w.Write(p)
if n.flusher != nil {
n.flusher.Flush()
}
return
}
func (n *noBufferWriter) Header() http.Header {
return n.w.Header()
}
func (n *noBufferWriter) WriteHeader(statusCode int) {
n.w.WriteHeader(statusCode)
}