Skip to content

Commit 24f71aa

Browse files
denrasemarandaneto
andauthored
Sanitize sensitive data from URLs (span desc, span data, crumbs, client errors) (#1327)
Co-authored-by: Manoel Aranda Neto <[email protected]> Co-authored-by: Manoel Aranda Neto <[email protected]>
1 parent df16b96 commit 24f71aa

25 files changed

+657
-84
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Sanitize sensitive data from URLs (span desc, span data, crumbs, client errors) ([#1327](https://github.com/getsentry/sentry-dart/pull/1327))
8+
59
### Dependencies
610

711
- Bump Cocoa SDK from v8.3.1 to v8.3.3 ([#1350](https://github.com/getsentry/sentry-dart/pull/1350), [#1355](https://github.com/getsentry/sentry-dart/pull/1355))

dart/lib/sentry.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ export 'src/exception_stacktrace_extractor.dart';
3939
// Isolates
4040
export 'src/sentry_isolate_extension.dart';
4141
export 'src/sentry_isolate.dart';
42+
// URL
43+
// ignore: invalid_export_of_internal_element
44+
export 'src/utils/http_sanitizer.dart';
45+
// ignore: invalid_export_of_internal_element
46+
export 'src/utils/url_details.dart';

dart/lib/sentry_io.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
// ignore: invalid_export_of_internal_element
12
export 'sentry.dart';
23
export 'src/sentry_attachment/io_sentry_attachment.dart';

dart/lib/src/event_processor/enricher/web_enricher_event_processor.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ class WebEnricherEventProcessor implements EnricherEventProcessor {
4949

5050
header.putIfAbsent('User-Agent', () => _window.navigator.userAgent);
5151

52-
return (request ?? SentryRequest()).copyWith(
53-
url: request?.url ?? _window.location.toString(),
54-
headers: header,
55-
);
52+
final url = request?.url ?? _window.location.toString();
53+
return (request ?? SentryRequest(url: url))
54+
.copyWith(
55+
headers: header,
56+
)
57+
.sanitized();
5658
}
5759

5860
SentryDevice _getDevice(SentryDevice? device) {

dart/lib/src/http_client/breadcrumb_client.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'package:http/http.dart';
22
import '../protocol.dart';
33
import '../hub.dart';
44
import '../hub_adapter.dart';
5+
import '../utils/url_details.dart';
6+
import '../utils/http_sanitizer.dart';
57

68
/// A [http](https://pub.dev/packages/http)-package compatible HTTP client
79
/// which records requests as breadcrumbs.
@@ -75,15 +77,20 @@ class BreadcrumbClient extends BaseClient {
7577
} finally {
7678
stopwatch.stop();
7779

80+
final urlDetails =
81+
HttpSanitizer.sanitizeUrl(request.url.toString()) ?? UrlDetails();
82+
7883
var breadcrumb = Breadcrumb.http(
7984
level: requestHadException ? SentryLevel.error : SentryLevel.info,
80-
url: request.url,
85+
url: Uri.parse(urlDetails.urlOrFallback),
8186
method: request.method,
8287
statusCode: statusCode,
8388
reason: reason,
8489
requestDuration: stopwatch.elapsed,
8590
requestBodySize: request.contentLength,
8691
responseBodySize: responseBodySize,
92+
httpQuery: urlDetails.query,
93+
httpFragment: urlDetails.fragment,
8794
);
8895

8996
await _hub.addBreadcrumb(breadcrumb);

dart/lib/src/http_client/tracing_client.dart

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import '../hub_adapter.dart';
44
import '../protocol.dart';
55
import '../tracing.dart';
66
import '../utils/tracing_utils.dart';
7+
import '../utils/http_sanitizer.dart';
78

89
/// A [http](https://pub.dev/packages/http)-package compatible HTTP client
910
/// which adds support to Sentry Performance feature.
@@ -19,17 +20,28 @@ class TracingClient extends BaseClient {
1920
@override
2021
Future<StreamedResponse> send(BaseRequest request) async {
2122
// see https://develop.sentry.dev/sdk/performance/#header-sentry-trace
23+
24+
final urlDetails = HttpSanitizer.sanitizeUrl(request.url.toString());
25+
26+
var description = request.method;
27+
if (urlDetails != null) {
28+
description += ' ${urlDetails.urlOrFallback}';
29+
}
30+
2231
final currentSpan = _hub.getSpan();
2332
var span = currentSpan?.startChild(
2433
'http.client',
25-
description: '${request.method} ${request.url}',
34+
description: description,
2635
);
2736

28-
// if the span is NoOp, we dont want to attach headers
37+
// if the span is NoOp, we don't want to attach headers
2938
if (span is NoOpSentrySpan) {
3039
span = null;
3140
}
3241

42+
span?.setData('method', request.method);
43+
urlDetails?.applyToSpan(span);
44+
3345
StreamedResponse? response;
3446
try {
3547
if (span != null) {

dart/lib/src/protocol/breadcrumb.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import 'package:meta/meta.dart';
33
import '../utils.dart';
44
import '../protocol.dart';
55

6-
/// Structed data to describe more information pior to the event captured.
6+
/// Structured data to describe more information prior to the event captured.
77
/// See `Sentry.captureEvent()`.
88
///
99
/// The outgoing JSON representation is:
@@ -47,6 +47,8 @@ class Breadcrumb {
4747

4848
// Size of the response body in bytes
4949
int? responseBodySize,
50+
String? httpQuery,
51+
String? httpFragment,
5052
}) {
5153
return Breadcrumb(
5254
type: 'http',
@@ -61,6 +63,8 @@ class Breadcrumb {
6163
if (requestDuration != null) 'duration': requestDuration.toString(),
6264
if (requestBodySize != null) 'request_body_size': requestBodySize,
6365
if (responseBodySize != null) 'response_body_size': responseBodySize,
66+
if (httpQuery != null) 'http.query': httpQuery,
67+
if (httpFragment != null) 'http.fragment': httpFragment,
6468
},
6569
);
6670
}

dart/lib/src/protocol/sentry_request.dart

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:meta/meta.dart';
22

33
import '../utils/iterable_extension.dart';
4+
import '../utils/http_sanitizer.dart';
45

56
/// The Request interface contains information on a HTTP request related to the event.
67
/// In client SDKs, this can be an outgoing request, or the request that rendered the current web page.
@@ -92,31 +93,18 @@ class SentryRequest {
9293
@Deprecated('Will be removed in v8. Use [data] instead')
9394
Map<String, String>? other,
9495
}) {
95-
// As far as I can tell there's no way to get the uri without the query part
96-
// so we replace it with an empty string.
97-
final urlWithoutQuery = uri
98-
.replace(query: '', fragment: '')
99-
.toString()
100-
.replaceAll('?', '')
101-
.replaceAll('#', '');
102-
103-
// Future proof, Dio does not support it yet and even if passing in the path,
104-
// the parsing of the uri returns empty.
105-
final query = uri.query.isEmpty ? null : uri.query;
106-
final fragment = uri.fragment.isEmpty ? null : uri.fragment;
107-
10896
return SentryRequest(
109-
url: urlWithoutQuery,
110-
fragment: fragment,
111-
queryString: query,
97+
url: uri.toString(),
11298
method: method,
11399
cookies: cookies,
114100
data: data,
115101
headers: headers,
116102
env: env,
103+
queryString: uri.query,
104+
fragment: uri.fragment,
117105
// ignore: deprecated_member_use_from_same_package
118106
other: other,
119-
);
107+
).sanitized();
120108
}
121109

122110
/// Deserializes a [SentryRequest] from JSON [Map].
@@ -162,12 +150,13 @@ class SentryRequest {
162150
Map<String, String>? env,
163151
@Deprecated('Will be removed in v8. Use [data] instead')
164152
Map<String, String>? other,
153+
bool removeCookies = false,
165154
}) =>
166155
SentryRequest(
167156
url: url ?? this.url,
168157
method: method ?? this.method,
169158
queryString: queryString ?? this.queryString,
170-
cookies: cookies ?? this.cookies,
159+
cookies: removeCookies ? null : cookies ?? this.cookies,
171160
data: data ?? _data,
172161
headers: headers ?? _headers,
173162
env: env ?? _env,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import 'package:meta/meta.dart';
2+
3+
import '../protocol.dart';
4+
import 'url_details.dart';
5+
6+
@internal
7+
class HttpSanitizer {
8+
static final RegExp _authRegExp = RegExp("(.+://)(.*@)(.*)");
9+
static final List<String> _securityHeaders = [
10+
"X-FORWARDED-FOR",
11+
"AUTHORIZATION",
12+
"COOKIE",
13+
"SET-COOKIE",
14+
"X-API-KEY",
15+
"X-REAL-IP",
16+
"REMOTE-ADDR",
17+
"FORWARDED",
18+
"PROXY-AUTHORIZATION",
19+
"X-CSRF-TOKEN",
20+
"X-CSRFTOKEN",
21+
"X-XSRF-TOKEN"
22+
];
23+
24+
/// Parse and sanitize url data for sentry.io
25+
static UrlDetails? sanitizeUrl(String? url) {
26+
if (url == null) {
27+
return null;
28+
}
29+
30+
final queryIndex = url.indexOf('?');
31+
final fragmentIndex = url.indexOf('#');
32+
33+
if (queryIndex > -1 && fragmentIndex > -1 && fragmentIndex < queryIndex) {
34+
// url considered malformed because of fragment position
35+
return UrlDetails();
36+
} else {
37+
try {
38+
final uri = Uri.parse(url);
39+
final urlWithAuthRemoved = _urlWithAuthRemoved(uri._url());
40+
return UrlDetails(
41+
url: urlWithAuthRemoved.isEmpty ? null : urlWithAuthRemoved,
42+
query: uri.query.isEmpty ? null : uri.query,
43+
fragment: uri.fragment.isEmpty ? null : uri.fragment);
44+
} catch (_) {
45+
return null;
46+
}
47+
}
48+
}
49+
50+
static Map<String, String>? sanitizedHeaders(Map<String, String>? headers) {
51+
if (headers == null) {
52+
return null;
53+
}
54+
final sanitizedHeaders = <String, String>{};
55+
headers.forEach((key, value) {
56+
if (!_securityHeaders.contains(key.toUpperCase())) {
57+
sanitizedHeaders[key] = value;
58+
}
59+
});
60+
return sanitizedHeaders;
61+
}
62+
63+
static String _urlWithAuthRemoved(String url) {
64+
final userInfoMatch = _authRegExp.firstMatch(url);
65+
if (userInfoMatch != null && userInfoMatch.groupCount == 3) {
66+
final userInfoString = userInfoMatch.group(2) ?? '';
67+
final replacementString = userInfoString.contains(":")
68+
? "[Filtered]:[Filtered]@"
69+
: "[Filtered]@";
70+
return '${userInfoMatch.group(1) ?? ''}$replacementString${userInfoMatch.group(3) ?? ''}';
71+
} else {
72+
return url;
73+
}
74+
}
75+
}
76+
77+
extension UriPath on Uri {
78+
String _url() {
79+
var buffer = '';
80+
if (scheme.isNotEmpty) {
81+
buffer += '$scheme://';
82+
}
83+
if (userInfo.isNotEmpty) {
84+
buffer += '$userInfo@';
85+
}
86+
buffer += host;
87+
if (path.isNotEmpty) {
88+
buffer += path;
89+
}
90+
return buffer;
91+
}
92+
}
93+
94+
extension SanitizedSentryRequest on SentryRequest {
95+
SentryRequest sanitized() {
96+
final urlDetails = HttpSanitizer.sanitizeUrl(url) ?? UrlDetails();
97+
return copyWith(
98+
url: urlDetails.urlOrFallback,
99+
queryString: urlDetails.query,
100+
fragment: urlDetails.fragment,
101+
headers: HttpSanitizer.sanitizedHeaders(headers),
102+
removeCookies: true,
103+
);
104+
}
105+
}

dart/lib/src/utils/url_details.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:meta/meta.dart';
2+
import '../../sentry.dart';
3+
4+
/// Sanitized url data for sentry.io
5+
@internal
6+
class UrlDetails {
7+
UrlDetails({this.url, this.query, this.fragment});
8+
9+
final String? url;
10+
final String? query;
11+
final String? fragment;
12+
13+
late final urlOrFallback = url ?? 'unknown';
14+
15+
void applyToSpan(ISentrySpan? span) {
16+
if (span == null) {
17+
return;
18+
}
19+
if (url != null) {
20+
span.setData('url', url);
21+
}
22+
if (query != null) {
23+
span.setData("http.query", query);
24+
}
25+
if (fragment != null) {
26+
span.setData("http.fragment", fragment);
27+
}
28+
}
29+
}

dart/test/event_processor/enricher/web_enricher_test.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,23 @@ void main() {
5757
expect(event.request?.url, 'foo.bar');
5858
});
5959

60+
test('does not add auth headers to request', () async {
61+
var event = SentryEvent(
62+
request: SentryRequest(
63+
url: 'foo.bar',
64+
headers: {
65+
'Authorization': 'foo',
66+
'authorization': 'bar',
67+
},
68+
),
69+
);
70+
var enricher = fixture.getSut();
71+
event = await enricher.apply(event);
72+
73+
expect(event.request?.headers['Authorization'], isNull);
74+
expect(event.request?.headers['authorization'], isNull);
75+
});
76+
6077
test('user-agent is not overridden if already present', () async {
6178
var event = SentryEvent(
6279
request: SentryRequest(

0 commit comments

Comments
 (0)