github.com/google/go-safeweb@v0.0.0-20231219055052-64d8cfc90fbb/safehttp/plugins/collector/collector.go (about)

     1  // Copyright 2020 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //	https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package collector provides a function for creating violation report handlers.
    16  // The created safehttp.Handler will be able to parse generic violation reports
    17  // as specified by https://w3c.github.io/reporting/ and CSP violation reports as
    18  // specified by https://www.w3.org/TR/CSP3/#deprecated-serialize-violation.
    19  package collector
    20  
    21  import (
    22  	"encoding/json"
    23  	"io/ioutil"
    24  
    25  	"github.com/google/go-safeweb/safehttp"
    26  )
    27  
    28  // Report represents a generic report as specified by https://w3c.github.io/reporting/#serialize-reports
    29  type Report struct {
    30  	// Type represents the type of the report. This will control how Body looks
    31  	// like.
    32  	Type string
    33  	// Age represents the number of milliseconds since the violation causing the
    34  	// report occured.
    35  	Age uint64
    36  	// URL is the address of the Document or Worker from which the report was
    37  	// generated.
    38  	URL string
    39  	// UserAgent contains the value of the User-Agent header of the request from
    40  	// which the report was generated.
    41  	UserAgent string
    42  	// Body contains the body of the report. This will be different for every Type.
    43  	// If Type is csp-violation then Body will be a CSPReport. Otherwise Body will
    44  	// be a map[string]interface{} containing the object that was passed, as unmarshalled
    45  	// using encoding/json.
    46  	Body interface{}
    47  }
    48  
    49  // CSPReport represents a CSP violation report as specified by https://www.w3.org/TR/CSP3/#deprecated-serialize-violation
    50  type CSPReport struct {
    51  	// BlockedURL is the URL of the resource that was blocked from loading by the
    52  	// Content Security Policy. If the blocked URL is from a different origin than
    53  	// the DocumentURL, the blocked URL is truncated to contain just the scheme,
    54  	// host and port.
    55  	BlockedURL string
    56  	// Disposition is either "enforce" or "report" depending on whether the Content-Security-Policy
    57  	// header or the Content-Security-Policy-Report-Only header is used.
    58  	Disposition string
    59  	// DocumentURL is the URL of the document in which the violation occurred.
    60  	DocumentURL string
    61  	// EffectiveDirective is the directive whose enforcement caused the violation.
    62  	EffectiveDirective string
    63  	// OriginalPolicy is the original policy as specified by the Content Security
    64  	// Policy header.
    65  	OriginalPolicy string
    66  	// Referrer is the referrer of the document in which the violation occurred.
    67  	Referrer string
    68  	// Sample is the first 40 characters of the inline script, event handler,
    69  	// or style that caused the violation.
    70  	Sample string
    71  	// StatusCode is the HTTP status code of the resource on which the global object
    72  	// was instantiated.
    73  	StatusCode uint
    74  	// ViolatedDirective is the name of the policy section that was violated.
    75  	ViolatedDirective string
    76  	// SourceFile represents the URL of the document or worker in which the violation
    77  	// was found.
    78  	SourceFile string
    79  	// LineNumber is the line number in the document or worker at which the violation
    80  	// occurred.
    81  	LineNumber uint
    82  	// ColumnNumber is the column number in the document or worker at which the violation
    83  	// occurred.
    84  	ColumnNumber uint
    85  }
    86  
    87  // Handler builds a safehttp.Handler which calls the given handler or cspHandler when
    88  // a violation report is received. Make sure to register the handler to receive POST
    89  // requests. If the handler recieves anything other than POST requests it will
    90  // respond with a 405 Method Not Allowed.
    91  func Handler(handler func(Report), cspHandler func(CSPReport)) safehttp.Handler {
    92  	return safehttp.HandlerFunc(func(w safehttp.ResponseWriter, r *safehttp.IncomingRequest) safehttp.Result {
    93  		if r.Method() != safehttp.MethodPost {
    94  			return w.WriteError(safehttp.StatusMethodNotAllowed)
    95  		}
    96  
    97  		b, err := ioutil.ReadAll(r.Body())
    98  		if err != nil {
    99  			return w.WriteError(safehttp.StatusBadRequest)
   100  		}
   101  
   102  		ct := r.Header.Get("Content-Type")
   103  		if ct == "application/csp-report" {
   104  			return handleDeprecatedCSPReports(cspHandler, w, b)
   105  		} else if ct == "application/reports+json" {
   106  			return handleReport(handler, w, b)
   107  		}
   108  
   109  		return w.WriteError(safehttp.StatusUnsupportedMediaType)
   110  	})
   111  }
   112  
   113  func handleDeprecatedCSPReports(h func(CSPReport), w safehttp.ResponseWriter, b []byte) safehttp.Result {
   114  	// In CSP2 it is clearly stated that a report has a single key 'csp-report'
   115  	// which holds the report object. Like this:
   116  	// {
   117  	//   "csp-report": {
   118  	//     // report goes here
   119  	//   }
   120  	// }
   121  	// Source: https://www.w3.org/TR/CSP2/#violation-reports
   122  	//
   123  	// But in the CSP3 spec this 'csp-report' key is never mentioned. So the report
   124  	// would look like this:
   125  	// {
   126  	//   // report goes here
   127  	// }
   128  	// Source: https://w3c.github.io/webappsec-csp/#deprecated-serialize-violation
   129  	//
   130  	// Because of this we have to support both. :/
   131  	r := struct {
   132  		CSPReport         json.RawMessage `json:"csp-report"`
   133  		BlockedURL        string          `json:"blocked-uri"`
   134  		Disposition       string          `json:"disposition"`
   135  		DocumentURL       string          `json:"document-uri"`
   136  		EffectiveDirectiv string          `json:"effective-directive"`
   137  		OriginalPolicy    string          `json:"original-policy"`
   138  		Referrer          string          `json:"referrer"`
   139  		Sample            string          `json:"script-sample"`
   140  		StatusCode        uint            `json:"status-code"`
   141  		ViolatedDirective string          `json:"violated-directive"`
   142  		SourceFile        string          `json:"source-file"`
   143  		LineNo            uint            `json:"lineno"`
   144  		LineNumber        uint            `json:"line-number"`
   145  		ColNo             uint            `json:"colno"`
   146  		ColumnNumber      uint            `json:"column-number"`
   147  	}{}
   148  	if err := json.Unmarshal(b, &r); err != nil {
   149  		return w.WriteError(safehttp.StatusBadRequest)
   150  	}
   151  
   152  	if len(r.CSPReport) != 0 {
   153  		if err := json.Unmarshal(r.CSPReport, &r); err != nil {
   154  			return w.WriteError(safehttp.StatusBadRequest)
   155  		}
   156  	}
   157  
   158  	ln := r.LineNo
   159  	if ln == 0 {
   160  		ln = r.LineNumber
   161  	}
   162  	cn := r.ColNo
   163  	if cn == 0 {
   164  		cn = r.ColumnNumber
   165  	}
   166  
   167  	rr := CSPReport{
   168  		BlockedURL:         r.BlockedURL,
   169  		Disposition:        r.Disposition,
   170  		DocumentURL:        r.DocumentURL,
   171  		EffectiveDirective: r.EffectiveDirectiv,
   172  		OriginalPolicy:     r.OriginalPolicy,
   173  		Referrer:           r.Referrer,
   174  		Sample:             r.Sample,
   175  		StatusCode:         r.StatusCode,
   176  		ViolatedDirective:  r.ViolatedDirective,
   177  		SourceFile:         r.SourceFile,
   178  		LineNumber:         ln,
   179  		ColumnNumber:       cn,
   180  	}
   181  	h(rr)
   182  
   183  	return w.Write(safehttp.NoContentResponse{})
   184  }
   185  
   186  var reportHandlers = map[string]func(json.RawMessage) (body interface{}, ok bool){
   187  	"csp-violation": cspViolationHandler,
   188  }
   189  
   190  func handleReport(h func(Report), w safehttp.ResponseWriter, b []byte) safehttp.Result {
   191  	var rList []struct {
   192  		Type      string          `json:"type"`
   193  		Age       uint64          `json:"age"`
   194  		URL       string          `json:"url"`
   195  		UserAgent string          `json:"userAgent"`
   196  		Body      json.RawMessage `json:"body"`
   197  	}
   198  	if err := json.Unmarshal(b, &rList); err != nil {
   199  		return w.WriteError(safehttp.StatusBadRequest)
   200  	}
   201  
   202  	badReport := false
   203  	for _, r := range rList {
   204  		reportToSend := Report{
   205  			Type:      r.Type,
   206  			Age:       r.Age,
   207  			URL:       r.URL,
   208  			UserAgent: r.UserAgent,
   209  		}
   210  
   211  		if f, ok := reportHandlers[r.Type]; ok {
   212  			b, ok := f(r.Body)
   213  			if !ok {
   214  				badReport = true
   215  				continue
   216  			}
   217  			reportToSend.Body = b
   218  		} else {
   219  			b := map[string]interface{}{}
   220  			if err := json.Unmarshal(r.Body, &b); err != nil {
   221  				badReport = true
   222  				continue
   223  			}
   224  			reportToSend.Body = b
   225  		}
   226  
   227  		h(reportToSend)
   228  	}
   229  
   230  	if badReport {
   231  		return w.WriteError(safehttp.StatusBadRequest)
   232  	}
   233  
   234  	return w.Write(safehttp.NoContentResponse{})
   235  }
   236  
   237  // cspViolationHandler parses reports of type csp-violation and returns a CSPReport.
   238  func cspViolationHandler(m json.RawMessage) (body interface{}, ok bool) {
   239  	// https://w3c.github.io/webappsec-csp/#reporting
   240  	r := struct {
   241  		BlockedURL         string `json:"blockedURL"`
   242  		Disposition        string `json:"disposition"`
   243  		DocumentURL        string `json:"documentURL"`
   244  		EffectiveDirective string `json:"effectiveDirective"`
   245  		OriginalPolicy     string `json:"originalPolicy"`
   246  		Referrer           string `json:"referrer"`
   247  		Sample             string `json:"sample"`
   248  		StatusCode         uint   `json:"statusCode"`
   249  		SourceFile         string `json:"sourceFile"`
   250  		LineNumber         uint   `json:"lineNumber"`
   251  		ColumnNumber       uint   `json:"columnNumber"`
   252  	}{}
   253  	if err := json.Unmarshal(m, &r); err != nil {
   254  		return nil, false
   255  	}
   256  
   257  	return CSPReport{
   258  		BlockedURL:         r.BlockedURL,
   259  		Disposition:        r.Disposition,
   260  		DocumentURL:        r.DocumentURL,
   261  		EffectiveDirective: r.EffectiveDirective,
   262  		OriginalPolicy:     r.OriginalPolicy,
   263  		Referrer:           r.Referrer,
   264  		Sample:             r.Sample,
   265  		StatusCode:         r.StatusCode,
   266  		// In CSP3 ViolatedDirective has been removed but is kept as
   267  		// a copy of EffectiveDirective for backwards compatibility.
   268  		ViolatedDirective: r.EffectiveDirective,
   269  		SourceFile:        r.SourceFile,
   270  		LineNumber:        r.LineNumber,
   271  		ColumnNumber:      r.ColumnNumber,
   272  	}, true
   273  }