-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #102 from fabiogermann/master
feat: added new CSP endpoint for reporting API
- Loading branch information
Showing
8 changed files
with
302 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
build/ | ||
dist/ | ||
csp_collector | ||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
package handler | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/jacobbednarz/go-csp-collector/internal/utils" | ||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
// CSPReport is the structure of the HTTP payload the system receives. | ||
type ReportAPIReports struct { | ||
Reports []ReportAPIReport `json:"reports"` | ||
} | ||
|
||
type ReportAPIReport struct { | ||
Age int `json:"age"` | ||
Body ReportAPIViolation `json:"body"` | ||
Type string `json:"type"` | ||
URL string `json:"url"` | ||
UserAgent string `json:"user_agent"` | ||
} | ||
|
||
type ReportAPIViolation struct { | ||
BlockedURL string `json:"blockedURL"` | ||
ColumnNumber int `json:"columnNumber,omitempty"` | ||
Disposition string `json:"disposition"` | ||
DocumentURL string `json:"documentURL"` | ||
EffectiveDirective string `json:"effectiveDirective"` | ||
LineNumber int `json:"lineNumber"` | ||
OriginalPolicy string `json:"originalPolicy"` | ||
Referrer string `json:"referrer"` | ||
Sample string `json:"sample,omitempty"` | ||
SourceFile string `json:"sourceFile"` | ||
StatusCode int `json:"statusCode"` | ||
} | ||
|
||
type ReportAPIViolationReportHandler struct { | ||
TruncateQueryStringFragment bool | ||
BlockedURIs []string | ||
|
||
LogClientIP bool | ||
LogTruncatedClientIP bool | ||
MetadataObject bool | ||
|
||
Logger *log.Logger | ||
} | ||
|
||
func (vrh *ReportAPIViolationReportHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
if r.Method != http.MethodPost { | ||
w.WriteHeader(http.StatusMethodNotAllowed) | ||
return | ||
} | ||
|
||
decoder := json.NewDecoder(r.Body) | ||
var reports_raw []ReportAPIReport | ||
|
||
err := decoder.Decode(&reports_raw) | ||
if err != nil { | ||
w.WriteHeader(http.StatusUnprocessableEntity) | ||
vrh.Logger.Debugf("unable to decode invalid JSON payload: %s", err) | ||
return | ||
} | ||
|
||
defer r.Body.Close() | ||
|
||
reports := ReportAPIReports{ | ||
Reports: reports_raw, | ||
} | ||
|
||
reportValidation := vrh.validateViolation(reports) | ||
if reportValidation != nil { | ||
http.Error(w, reportValidation.Error(), http.StatusBadRequest) | ||
vrh.Logger.Debugf("received invalid payload: %s", reportValidation.Error()) | ||
return | ||
} | ||
|
||
var metadata interface{} | ||
if vrh.MetadataObject { | ||
metadataMap := make(map[string]string) | ||
query := r.URL.Query() | ||
|
||
for k, v := range query { | ||
metadataMap[k] = v[0] | ||
} | ||
|
||
metadata = metadataMap | ||
} else { | ||
metadatas, gotMetadata := r.URL.Query()["metadata"] | ||
if gotMetadata { | ||
metadata = metadatas[0] | ||
} | ||
} | ||
|
||
for _, violation := range reports.Reports { | ||
report_only := violation.Body.Disposition == "report" | ||
lf := log.Fields{ | ||
"report_only": report_only, | ||
"document_uri": violation.Body.DocumentURL, | ||
"referrer": violation.Body.Referrer, | ||
"blocked_uri": violation.Body.BlockedURL, | ||
"violated_directive": violation.Body.EffectiveDirective, | ||
"effective_directive": violation.Body.EffectiveDirective, | ||
"original_policy": violation.Body.OriginalPolicy, | ||
"disposition": violation.Body.Disposition, | ||
"status_code": violation.Body.StatusCode, | ||
"source_file": violation.Body.SourceFile, | ||
"line_number": violation.Body.LineNumber, | ||
"column_number": violation.Body.ColumnNumber, | ||
"metadata": metadata, | ||
"path": r.URL.Path, | ||
} | ||
|
||
if vrh.TruncateQueryStringFragment { | ||
lf["document_uri"] = utils.TruncateQueryStringFragment(violation.Body.DocumentURL) | ||
lf["referrer"] = utils.TruncateQueryStringFragment(violation.Body.Referrer) | ||
lf["blocked_uri"] = utils.TruncateQueryStringFragment(violation.Body.BlockedURL) | ||
lf["source_file"] = utils.TruncateQueryStringFragment(violation.Body.SourceFile) | ||
} | ||
|
||
if vrh.LogClientIP { | ||
ip, err := utils.GetClientIP(r) | ||
if err != nil { | ||
vrh.Logger.Warnf("unable to parse client ip: %s", err) | ||
} | ||
lf["client_ip"] = ip.String() | ||
} | ||
|
||
if vrh.LogTruncatedClientIP { | ||
ip, err := utils.GetClientIP(r) | ||
if err != nil { | ||
vrh.Logger.Warnf("unable to parse client ip: %s", err) | ||
} | ||
lf["client_ip"] = utils.TruncateClientIP(ip) | ||
} | ||
|
||
vrh.Logger.WithFields(lf).Info() | ||
} | ||
} | ||
|
||
func (vrh *ReportAPIViolationReportHandler) validateViolation(r ReportAPIReports) error { | ||
for _, violation := range r.Reports { | ||
if violation.Type != "csp-violation" { | ||
continue // Skip the rest of the loop and move to the next iteration | ||
} | ||
for _, value := range vrh.BlockedURIs { | ||
if strings.HasPrefix(violation.Body.BlockedURL, value) { | ||
err := fmt.Errorf("blocked URI ('%s') is an invalid resource", value) | ||
return err | ||
} | ||
} | ||
if !strings.HasPrefix(violation.Body.DocumentURL, "http") { | ||
return fmt.Errorf("document URI ('%s') is invalid", violation.Body.DocumentURL) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func ReportAPICorsHandler(w http.ResponseWriter, r *http.Request) { | ||
origin := r.Header.Get("Origin") | ||
method := r.Header.Get("Access-Control-Request-Method") | ||
header := r.Header.Get("Access-Control-Request-Headers") | ||
allow_origin := utils.Ternary(origin != "" && utils.ValidateOrigin(origin), origin, "*") | ||
allow_method := utils.Ternary(method != "", method, "*") | ||
allow_header := utils.Ternary(header != "", header, "*") | ||
// Special handling due to bug in Chrome | ||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1152867 | ||
w.Header().Set("Access-Control-Allow-Origin", allow_origin) | ||
w.Header().Set("Access-Control-Allow-Methods", allow_method) | ||
w.Header().Set("Access-Control-Max-Age", "60") | ||
w.Header().Set("Access-Control-Allow-Headers", allow_header) | ||
w.Header().Set("vary", "Origin, Access-Control-Request-Method, Access-Control-Request-Headers") | ||
|
||
w.Header().Set("Cross-Origin-Resource-Policy", "cross-origin") | ||
w.Header().Set("Content-Type", "text/plain;charset=UTF-8") | ||
w.Header().Set("Server", "go-csp-collector") | ||
w.WriteHeader(http.StatusOK) | ||
_, _ = w.Write([]byte("OK")) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package handler | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
func TestReportAPICspReport(t *testing.T) { | ||
rawReport := []byte(`[ | ||
{ | ||
"age": 156165, | ||
"body": { | ||
"blockedURL": "inline", | ||
"disposition": "report", | ||
"documentURL": "https://integrations.miro.com/asana-cards/miro-plugin.html", | ||
"effectiveDirective": "script-src-elem", | ||
"lineNumber": 1, | ||
"originalPolicy": "default-src 'self'; script-src 'self'; report-to csp-endpoint2;", | ||
"referrer": "https://miro.com/", | ||
"sample": "", | ||
"sourceFile": "https://integrations.miro.com/asana-cards/miro-plugin.html", | ||
"statusCode": 200 | ||
}, | ||
"type": "csp-violation", | ||
"url": "https://integrations.miro.com/asana-cards/miro-plugin.html", | ||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" | ||
}, | ||
{ | ||
"age": 156165, | ||
"body": { | ||
"blockedURL": "https://static.miro-apps.com/integrations/asana-addon/js/miro-plugin.a8cdc6de401c0d820778.js", | ||
"disposition": "report", | ||
"documentURL": "https://integrations.miro.com/asana-cards/miro-plugin.html", | ||
"effectiveDirective": "script-src-elem", | ||
"originalPolicy": "default-src 'self'; script-src 'self'; report-to csp-endpoint2;", | ||
"referrer": "https://miro.com/", | ||
"sample": "", | ||
"statusCode": 200 | ||
}, | ||
"type": "csp-violation", | ||
"url": "https://integrations.miro.com/asana-cards/miro-plugin.html", | ||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" | ||
}, | ||
{ | ||
"age": 156165, | ||
"body": { | ||
"blockedURL": "https://miro.com/app/static/sdk.1.1.js", | ||
"disposition": "report", | ||
"documentURL": "https://integrations.miro.com/asana-cards/miro-plugin.html", | ||
"effectiveDirective": "script-src-elem", | ||
"originalPolicy": "default-src 'self'; script-src 'self'; report-to csp-endpoint2;", | ||
"referrer": "https://miro.com/", | ||
"sample": "", | ||
"statusCode": 200 | ||
}, | ||
"type": "csp-violation", | ||
"url": "https://integrations.miro.com/asana-cards/miro-plugin.html", | ||
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" | ||
} | ||
]`) | ||
|
||
var reports_raw []ReportAPIReport | ||
jsonErr := json.Unmarshal(rawReport, &reports_raw) | ||
if jsonErr != nil { | ||
fmt.Println("error:", jsonErr) | ||
} | ||
|
||
reports := ReportAPIReports{ | ||
Reports: reports_raw, | ||
} | ||
|
||
reportApiViolationHandler := &ReportAPIViolationReportHandler{BlockedURIs: invalidBlockedURIs} | ||
validateErr := reportApiViolationHandler.validateViolation(reports) | ||
if validateErr != nil { | ||
t.Errorf("expected error not be raised") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters