@@ -25,7 +25,8 @@ func init() {
2525 httpcaddyfile .RegisterHandlerDirective (headlessLogDownloadModule , parseCaddyfile )
2626}
2727
28- // HeadlessLogDownload implements an HTTP handler that extracts gitpod headers
28+ // HeadlessLogDownload implements an HTTP handler that proxies headless log downloads
29+ // with security headers to prevent XSS attacks from malicious branch names in logs.
2930type HeadlessLogDownload struct {
3031 Service string `json:"service,omitempty"`
3132}
@@ -93,6 +94,9 @@ func (m HeadlessLogDownload) ServeHTTP(w http.ResponseWriter, r *http.Request, n
9394 return caddyhttp .Error (http .StatusInternalServerError , fmt .Errorf ("unexpected error downloading prebuild log" ))
9495 }
9596
97+ setSecurityHeaders (w )
98+ copyResponseHeaders (w , resp )
99+
96100 brw := newNoBufferResponseWriter (w )
97101 _ , err = io .Copy (brw , resp .Body )
98102 if err != nil {
@@ -103,6 +107,38 @@ func (m HeadlessLogDownload) ServeHTTP(w http.ResponseWriter, r *http.Request, n
103107 return next .ServeHTTP (w , r )
104108}
105109
110+ func setSecurityHeaders (w http.ResponseWriter ) {
111+ headers := w .Header ()
112+ headers .Set ("Content-Type" , "text/plain; charset=utf-8" )
113+ headers .Set ("X-Content-Type-Options" , "nosniff" )
114+ headers .Set ("X-Frame-Options" , "DENY" )
115+ headers .Set ("Content-Security-Policy" , "default-src 'none'; style-src 'unsafe-inline'" )
116+ headers .Set ("Referrer-Policy" , "strict-origin-when-cross-origin" )
117+ headers .Set ("Cache-Control" , "no-cache, no-store, must-revalidate" )
118+ }
119+
120+ // copyResponseHeaders copies safe headers from upstream response, excluding potentially dangerous ones
121+ func copyResponseHeaders (w http.ResponseWriter , resp * http.Response ) {
122+ // List of safe headers to copy from upstream
123+ safeHeaders := []string {
124+ "Content-Length" ,
125+ "Content-Encoding" ,
126+ "Content-Disposition" ,
127+ "Last-Modified" ,
128+ "ETag" ,
129+ }
130+
131+ destHeaders := w .Header ()
132+ for _ , header := range safeHeaders {
133+ if value := resp .Header .Get (header ); value != "" {
134+ destHeaders .Set (header , value )
135+ }
136+ }
137+
138+ // Note: We intentionally do NOT copy Content-Type from upstream
139+ // because we want to enforce text/plain for security
140+ }
141+
106142// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
107143func (m * HeadlessLogDownload ) UnmarshalCaddyfile (d * caddyfile.Dispenser ) error {
108144 if ! d .Next () {
@@ -172,3 +208,11 @@ func (n *noBufferWriter) Write(p []byte) (written int, err error) {
172208
173209 return
174210}
211+
212+ func (n * noBufferWriter ) Header () http.Header {
213+ return n .w .Header ()
214+ }
215+
216+ func (n * noBufferWriter ) WriteHeader (statusCode int ) {
217+ n .w .WriteHeader (statusCode )
218+ }
0 commit comments