mirror of
https://github.com/oneclickvirt/backtrace.git
synced 2026-07-23 11:20:13 +08:00
fix: redact structured source details
This commit is contained in:
parent
f0ba5fa410
commit
c457682ded
@ -2,8 +2,11 @@ package bgptools
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@ -14,6 +17,53 @@ func (fn privacyRoundTripper) RoundTrip(request *http.Request) (*http.Response,
|
|||||||
return fn(request)
|
return fn(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructuredReportErrorsDoNotExposeRemoteURLs(t *testing.T) {
|
||||||
|
var rdap *httptest.Server
|
||||||
|
rdap = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
_, _ = fmt.Fprintf(w, `{"handle":"TEST","links":[{"rel":"geofeed","href":"https://private.example/geofeed?token=secret"}]}`)
|
||||||
|
}))
|
||||||
|
defer rdap.Close()
|
||||||
|
failing := &http.Client{Transport: privacyRoundTripper(func(request *http.Request) (*http.Response, error) {
|
||||||
|
return nil, errors.New("dial " + request.URL.String())
|
||||||
|
})}
|
||||||
|
report, err := QueryIPBGPReport(context.Background(), "192.0.2.1", IPBGPReportConfig{
|
||||||
|
RDAPClient: rdap.Client(),
|
||||||
|
RDAPBaseURL: rdap.URL,
|
||||||
|
FetchGeofeed: true,
|
||||||
|
GeofeedClient: failing,
|
||||||
|
ResolveASN: func(context.Context, string) (string, error) {
|
||||||
|
return "", errors.New("resolver https://private.example/asn?key=secret")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, source := range report.Sources {
|
||||||
|
for _, field := range []string{source.Source, source.Error} {
|
||||||
|
for _, forbidden := range []string{"private.example", "token=", "key=", "secret"} {
|
||||||
|
if strings.Contains(field, forbidden) {
|
||||||
|
t.Fatalf("structured source leaked %q: %+v", forbidden, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(report.Geofeeds) != 1 || report.Geofeeds[0].Error != "request_failed" {
|
||||||
|
t.Fatalf("geofeed error was not stabilized: %+v", report.Geofeeds)
|
||||||
|
}
|
||||||
|
encoded, err := json.Marshal(report)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, forbidden := range []string{"token=", "key=", "secret", "userinfo@"} {
|
||||||
|
if strings.Contains(string(encoded), forbidden) {
|
||||||
|
t.Fatalf("structured report leaked %q: %s", forbidden, encoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if report.RDAP == nil || len(report.RDAP.GeofeedURLs) != 1 || report.RDAP.GeofeedURLs[0] != "https://private.example/geofeed" || report.Geofeeds[0].URL != "https://private.example/geofeed" {
|
||||||
|
t.Fatalf("geofeed URL was not sanitized consistently: %+v", report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRemoteFetchErrorsDoNotExposeSourceURL(t *testing.T) {
|
func TestRemoteFetchErrorsDoNotExposeSourceURL(t *testing.T) {
|
||||||
client := &http.Client{Transport: privacyRoundTripper(func(request *http.Request) (*http.Response, error) {
|
client := &http.Client{Transport: privacyRoundTripper(func(request *http.Request) (*http.Response, error) {
|
||||||
return nil, errors.New("dial " + request.URL.String())
|
return nil, errors.New("dial " + request.URL.String())
|
||||||
|
|||||||
@ -128,7 +128,7 @@ func parseRDAPRecord(ip, source string, data []byte) (RDAPRecord, error) {
|
|||||||
record := RDAPRecord{
|
record := RDAPRecord{
|
||||||
IP: ip, Handle: payload.Handle, Name: payload.Name, Type: payload.Type,
|
IP: ip, Handle: payload.Handle, Name: payload.Name, Type: payload.Type,
|
||||||
Country: payload.Country, StartAddress: payload.StartAddress, EndAddress: payload.EndAddress,
|
Country: payload.Country, StartAddress: payload.StartAddress, EndAddress: payload.EndAddress,
|
||||||
ParentHandle: payload.ParentHandle, Port43: payload.Port43, Status: payload.Status, Source: source,
|
ParentHandle: payload.ParentHandle, Port43: payload.Port43, Status: payload.Status, Source: "rdap",
|
||||||
}
|
}
|
||||||
for _, event := range payload.Events {
|
for _, event := range payload.Events {
|
||||||
switch strings.ToLower(event.Action) {
|
switch strings.ToLower(event.Action) {
|
||||||
|
|||||||
@ -131,7 +131,7 @@ func QueryASNRelationships(ctx context.Context, asn string, cfg RelationshipConf
|
|||||||
report := &ASNRelationshipReport{TargetASN: normalized}
|
report := &ASNRelationshipReport{TargetASN: normalized}
|
||||||
ripeURL, err := addQuery(cfg.RIPEstatURL, "resource", normalized)
|
ripeURL, err := addQuery(cfg.RIPEstatURL, "resource", normalized)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.New("invalid relationship source configuration")
|
||||||
}
|
}
|
||||||
ripeRaw, ripeStatus, ripeErr := fetchRelationshipJSON(ctx, cfg, "ripestat", ripeURL)
|
ripeRaw, ripeStatus, ripeErr := fetchRelationshipJSON(ctx, cfg, "ripestat", ripeURL)
|
||||||
report.SourceStatuses = append(report.SourceStatuses, sourceStatus("ripestat", ripeStatus, ripeErr))
|
report.SourceStatuses = append(report.SourceStatuses, sourceStatus("ripestat", ripeStatus, ripeErr))
|
||||||
@ -153,7 +153,7 @@ func QueryASNRelationships(ctx context.Context, asn string, cfg RelationshipConf
|
|||||||
|
|
||||||
peeringURL, err := addQuery(cfg.PeeringDBURL, "asn", normalized)
|
peeringURL, err := addQuery(cfg.PeeringDBURL, "asn", normalized)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.New("invalid relationship source configuration")
|
||||||
}
|
}
|
||||||
peeringRaw, peeringStatus, peeringErr := fetchRelationshipJSON(ctx, cfg, "peeringdb", peeringURL)
|
peeringRaw, peeringStatus, peeringErr := fetchRelationshipJSON(ctx, cfg, "peeringdb", peeringURL)
|
||||||
report.SourceStatuses = append(report.SourceStatuses, sourceStatus("peeringdb", peeringStatus, peeringErr))
|
report.SourceStatuses = append(report.SourceStatuses, sourceStatus("peeringdb", peeringStatus, peeringErr))
|
||||||
@ -255,7 +255,16 @@ func sourceStatus(source string, status RelationshipStatus, err error) Relations
|
|||||||
Timeout: status == RelationshipTimeout,
|
Timeout: status == RelationshipTimeout,
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
item.Error = err.Error()
|
switch status {
|
||||||
|
case RelationshipRateLimited:
|
||||||
|
item.Error = "rate_limited"
|
||||||
|
case RelationshipTimeout:
|
||||||
|
item.Error = "timeout"
|
||||||
|
case RelationshipMissingFields:
|
||||||
|
item.Error = "missing_fields"
|
||||||
|
default:
|
||||||
|
item.Error = "request_failed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|||||||
@ -217,6 +217,12 @@ func QueryIPBGPReport(parent context.Context, ip string, cfg IPBGPReportConfig)
|
|||||||
geofeedURLs = append(geofeedURLs, report.WHOIS.GeofeedURLs...)
|
geofeedURLs = append(geofeedURLs, report.WHOIS.GeofeedURLs...)
|
||||||
}
|
}
|
||||||
geofeedURLs = uniqueSortedStrings(geofeedURLs)
|
geofeedURLs = uniqueSortedStrings(geofeedURLs)
|
||||||
|
if report.RDAP != nil {
|
||||||
|
report.RDAP.GeofeedURLs = sanitizeReportURLs(report.RDAP.GeofeedURLs)
|
||||||
|
}
|
||||||
|
if report.WHOIS != nil {
|
||||||
|
report.WHOIS.GeofeedURLs = sanitizeReportURLs(report.WHOIS.GeofeedURLs)
|
||||||
|
}
|
||||||
for _, geofeedURL := range geofeedURLs {
|
for _, geofeedURL := range geofeedURLs {
|
||||||
if !cfg.FetchGeofeed {
|
if !cfg.FetchGeofeed {
|
||||||
report.Geofeeds = append(report.Geofeeds, GeofeedResult{URL: geofeedURL, Status: ReportUnsupported, Error: "fetch disabled"})
|
report.Geofeeds = append(report.Geofeeds, GeofeedResult{URL: geofeedURL, Status: ReportUnsupported, Error: "fetch disabled"})
|
||||||
@ -224,7 +230,7 @@ func QueryIPBGPReport(parent context.Context, ip string, cfg IPBGPReportConfig)
|
|||||||
}
|
}
|
||||||
result := fetchGeofeed(ctx, geofeedURL, cfg)
|
result := fetchGeofeed(ctx, geofeedURL, cfg)
|
||||||
report.Geofeeds = append(report.Geofeeds, result)
|
report.Geofeeds = append(report.Geofeeds, result)
|
||||||
report.Sources = append(report.Sources, IPBGPSourceStatus{Source: "geofeed:" + geofeedURL, Status: result.Status, Error: result.Error})
|
report.Sources = append(report.Sources, IPBGPSourceStatus{Source: "geofeed", Status: result.Status, Error: result.Error})
|
||||||
}
|
}
|
||||||
|
|
||||||
asn := strings.TrimSpace(cfg.ASN)
|
asn := strings.TrimSpace(cfg.ASN)
|
||||||
@ -286,7 +292,7 @@ func inferRIR(record RDAPRecord, available bool) RIRInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig) GeofeedResult {
|
func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig) GeofeedResult {
|
||||||
result := GeofeedResult{URL: rawURL}
|
result := GeofeedResult{URL: sanitizeReportURL(rawURL)}
|
||||||
u, err := url.Parse(rawURL)
|
u, err := url.Parse(rawURL)
|
||||||
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
|
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
|
||||||
result.Status = ReportUnsupported
|
result.Status = ReportUnsupported
|
||||||
@ -310,7 +316,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
|
|||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Status = ReportError
|
result.Status = ReportError
|
||||||
result.Error = err.Error()
|
result.Error = stableReportError(result.Status, err)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
req.Header.Set("Accept", "text/csv, text/plain, */*")
|
req.Header.Set("Accept", "text/csv, text/plain, */*")
|
||||||
@ -318,7 +324,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
|
|||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Status = reportStatusForError(err)
|
result.Status = reportStatusForError(err)
|
||||||
result.Error = err.Error()
|
result.Error = stableReportError(result.Status, err)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@ -341,7 +347,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
|
|||||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
|
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Status = reportStatusForError(err)
|
result.Status = reportStatusForError(err)
|
||||||
result.Error = err.Error()
|
result.Error = stableReportError(result.Status, err)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
if int64(len(body)) > maxBytes {
|
if int64(len(body)) > maxBytes {
|
||||||
@ -517,11 +523,51 @@ func rirNameFromText(value string) string {
|
|||||||
func sourceStatusForError(source string, status ReportStatus, err error) IPBGPSourceStatus {
|
func sourceStatusForError(source string, status ReportStatus, err error) IPBGPSourceStatus {
|
||||||
item := IPBGPSourceStatus{Source: source, Status: status}
|
item := IPBGPSourceStatus{Source: source, Status: status}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
item.Error = err.Error()
|
item.Error = stableReportError(status, err)
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stableReportError(status ReportStatus, err error) string {
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return "canceled"
|
||||||
|
}
|
||||||
|
switch status {
|
||||||
|
case ReportRateLimited:
|
||||||
|
return "rate_limited"
|
||||||
|
case ReportTimeout:
|
||||||
|
return "timeout"
|
||||||
|
case ReportUnsupported:
|
||||||
|
return "unsupported"
|
||||||
|
case ReportMissingFields:
|
||||||
|
return "missing_fields"
|
||||||
|
default:
|
||||||
|
return "request_failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeReportURL(raw string) string {
|
||||||
|
parsed, err := url.Parse(strings.TrimSpace(raw))
|
||||||
|
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
parsed.User = nil
|
||||||
|
parsed.RawQuery = ""
|
||||||
|
parsed.ForceQuery = false
|
||||||
|
parsed.Fragment = ""
|
||||||
|
return parsed.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeReportURLs(values []string) []string {
|
||||||
|
result := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if sanitized := sanitizeReportURL(value); sanitized != "" {
|
||||||
|
result = append(result, sanitized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueSortedStrings(result)
|
||||||
|
}
|
||||||
|
|
||||||
func reportStatusForError(err error) ReportStatus {
|
func reportStatusForError(err error) ReportStatus {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return ReportAvailable
|
return ReportAvailable
|
||||||
|
|||||||
@ -2,7 +2,7 @@ package model
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
const BackTraceVersion = "v0.0.16"
|
const BackTraceVersion = "v0.0.17"
|
||||||
|
|
||||||
var EnableLoger = false
|
var EnableLoger = false
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user