1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detection.h"
10 #include "base/base64.h"
12 #include "base/metrics/histogram.h"
13 #include "base/metrics/sparse_histogram.h"
14 #include "base/strings/string_number_conversions.h"
15 #include "base/strings/string_util.h"
16 #include "components/data_reduction_proxy/common/data_reduction_proxy_headers.h"
17 #include "net/http/http_response_headers.h"
18 #include "net/http/http_util.h"
20 #if defined(OS_ANDROID)
21 #include "net/android/network_library.h"
24 // Macro for UMA reporting. HTTP response first reports to histogram events
25 // |http_histogram| by |carrier_id|; then reports the total counts to
26 // |http_histogram|_Total. HTTPS response reports to histograms
27 // |https_histogram| and |https_histogram|_Total similarly.
28 #define REPORT_TAMPER_DETECTION_UMA( \
29 scheme_is_https, https_histogram, http_histogram, carrier_id) \
31 if (scheme_is_https) { \
32 UMA_HISTOGRAM_SPARSE_SLOWLY(https_histogram, carrier_id); \
33 UMA_HISTOGRAM_COUNTS(https_histogram "_Total", 1); \
35 UMA_HISTOGRAM_SPARSE_SLOWLY(http_histogram, carrier_id); \
36 UMA_HISTOGRAM_COUNTS(http_histogram "_Total", 1); \
40 namespace data_reduction_proxy
{
43 bool DataReductionProxyTamperDetection::DetectAndReport(
44 const net::HttpResponseHeaders
* headers
,
45 const bool scheme_is_https
) {
47 // Abort tamper detection, if the fingerprint of the Chrome-Proxy header is
49 std::string chrome_proxy_fingerprint
;
50 if (!GetDataReductionProxyActionFingerprintChromeProxy(
51 headers
, &chrome_proxy_fingerprint
)) {
56 unsigned carrier_id
= 0;
57 #if defined(OS_ANDROID)
58 base::StringToUint(net::android::GetTelephonyNetworkOperator(), &carrier_id
);
61 DataReductionProxyTamperDetection
tamper_detection(
62 headers
, scheme_is_https
, carrier_id
);
64 // Checks if the Chrome-Proxy header has been tampered with.
65 if (tamper_detection
.ValidateChromeProxyHeader(chrome_proxy_fingerprint
)) {
66 tamper_detection
.ReportUMAforChromeProxyHeaderValidation();
70 // Chrome-Proxy header has not been tampered with, and thus other
71 // fingerprints are valid. Reports the number of responses that other
72 // fingerprints will be checked.
73 REPORT_TAMPER_DETECTION_UMA(
75 "DataReductionProxy.HeaderTamperDetectionHTTPS",
76 "DataReductionProxy.HeaderTamperDetectionHTTP",
79 bool tampered
= false;
80 std::string fingerprint
;
82 if (GetDataReductionProxyActionFingerprintVia(headers
, &fingerprint
)) {
83 bool has_chrome_proxy_via_header
;
84 if (tamper_detection
.ValidateViaHeader(
85 fingerprint
, &has_chrome_proxy_via_header
)) {
86 tamper_detection
.ReportUMAforViaHeaderValidation(
87 has_chrome_proxy_via_header
);
92 if (GetDataReductionProxyActionFingerprintOtherHeaders(
93 headers
, &fingerprint
)) {
94 if (tamper_detection
.ValidateOtherHeaders(fingerprint
)) {
95 tamper_detection
.ReportUMAforOtherHeadersValidation();
100 if (GetDataReductionProxyActionFingerprintContentLength(
101 headers
, &fingerprint
)) {
102 if (tamper_detection
.ValidateContentLengthHeader(fingerprint
)) {
103 tamper_detection
.ReportUMAforContentLengthHeaderValidation();
109 REPORT_TAMPER_DETECTION_UMA(
111 "DataReductionProxy.HeaderTamperDetectionPassHTTPS",
112 "DataReductionProxy.HeaderTamperDetectionPassHTTP",
119 // Constructor initializes the map of fingerprint names to codes.
120 DataReductionProxyTamperDetection::DataReductionProxyTamperDetection(
121 const net::HttpResponseHeaders
* headers
,
122 const bool is_secure
,
123 const unsigned carrier_id
)
124 : response_headers_(headers
),
125 scheme_is_https_(is_secure
),
126 carrier_id_(carrier_id
) {
130 DataReductionProxyTamperDetection::~DataReductionProxyTamperDetection() {};
132 // |fingerprint| is Base64 encoded. Decodes it first. Then calculates the
133 // fingerprint of received Chrome-Proxy header, and compares the two to see
134 // whether they are equal or not.
135 bool DataReductionProxyTamperDetection::ValidateChromeProxyHeader(
136 const std::string
& fingerprint
) const {
137 std::string received_fingerprint
;
138 if (!base::Base64Decode(fingerprint
, &received_fingerprint
))
141 // Gets the Chrome-Proxy header values with its fingerprint removed.
142 std::vector
<std::string
> chrome_proxy_header_values
;
143 GetDataReductionProxyHeaderWithFingerprintRemoved(
144 response_headers_
, &chrome_proxy_header_values
);
146 // Calculates the MD5 hash value of Chrome-Proxy.
147 std::string actual_fingerprint
;
148 GetMD5(ValuesToSortedString(&chrome_proxy_header_values
),
149 &actual_fingerprint
);
151 return received_fingerprint
!= actual_fingerprint
;
154 void DataReductionProxyTamperDetection::
155 ReportUMAforChromeProxyHeaderValidation() const {
156 REPORT_TAMPER_DETECTION_UMA(
158 "DataReductionProxy.HeaderTamperedHTTPS_ChromeProxy",
159 "DataReductionProxy.HeaderTamperedHTTP_ChromeProxy",
163 // Checks whether there are other proxies/middleboxes' named after the data
164 // reduction proxy's name in Via header. |has_chrome_proxy_via_header| marks
165 // that whether the data reduction proxy's Via header occurs or not.
166 bool DataReductionProxyTamperDetection::ValidateViaHeader(
167 const std::string
& fingerprint
,
168 bool* has_chrome_proxy_via_header
) const {
169 bool has_intermediary
;
170 *has_chrome_proxy_via_header
= HasDataReductionProxyViaHeader(
174 if (*has_chrome_proxy_via_header
)
175 return !has_intermediary
;
179 void DataReductionProxyTamperDetection::ReportUMAforViaHeaderValidation(
180 bool has_chrome_proxy
) const {
181 // The Via header of the data reduction proxy is missing.
182 if (!has_chrome_proxy
) {
183 REPORT_TAMPER_DETECTION_UMA(
185 "DataReductionProxy.HeaderTamperedHTTPS_Via_Missing",
186 "DataReductionProxy.HeaderTamperedHTTP_Via_Missing",
191 REPORT_TAMPER_DETECTION_UMA(
193 "DataReductionProxy.HeaderTamperedHTTPS_Via",
194 "DataReductionProxy.HeaderTamperedHTTP_Via",
198 // The data reduction proxy constructs a canonical representation of values of
199 // a list of headers. The fingerprint is constructed as follows:
200 // 1) for each header, gets the string representation of its values (same as
201 // ValuesToSortedString);
202 // 2) concatenates all header's string representations using ";" as a delimiter;
203 // 3) calculates the MD5 hash value of above concatenated string;
204 // 4) appends the header names to the fingerprint, with a delimiter "|".
205 // The constructed fingerprint looks like:
206 // [hashed_fingerprint]|header_name1|header_namer2:...
208 // To check whether such a fingerprint matches the response that the Chromium
209 // client receives, the client firstly extracts the header names. For
210 // each header, gets its string representation (by ValuesToSortedString),
211 // concatenates them and calculates the MD5 hash value. Compares the hash
212 // value to the fingerprint received from the data reduction proxy.
213 bool DataReductionProxyTamperDetection::ValidateOtherHeaders(
214 const std::string
& fingerprint
) const {
215 DCHECK(!fingerprint
.empty());
217 // According to RFC 2616, "|" is not a valid character in a header name; and
218 // it is not a valid base64 encoding character, so there is no ambituity in
219 //using it as a delimiter.
220 net::HttpUtil::ValuesIterator
it(
221 fingerprint
.begin(), fingerprint
.end(), '|');
223 // The first value is the base64 encoded fingerprint.
224 std::string received_fingerprint
;
226 !base::Base64Decode(it
.value(), &received_fingerprint
)) {
231 std::string header_values
;
232 // The following values are the header names included in the fingerprint
234 while (it
.GetNext()) {
235 // Gets values of one header.
236 std::vector
<std::string
> response_header_values
=
237 GetHeaderValues(response_headers_
, it
.value());
238 // Sorts the values and concatenate them, with delimiter ";". ";" can occur
239 // in a header value and thus two different sets of header values could map
240 // to the same string representation. This should be very rare.
241 // TODO(xingx): find an unambiguous representation.
242 header_values
+= ValuesToSortedString(&response_header_values
) + ";";
245 // Calculates the MD5 hash of the concatenated string.
246 std::string actual_fingerprint
;
247 GetMD5(header_values
, &actual_fingerprint
);
249 return received_fingerprint
!= actual_fingerprint
;
252 void DataReductionProxyTamperDetection::
253 ReportUMAforOtherHeadersValidation() const {
254 REPORT_TAMPER_DETECTION_UMA(
256 "DataReductionProxy.HeaderTamperedHTTPS_OtherHeaders",
257 "DataReductionProxy.HeaderTamperedHTTP_OtherHeaders",
261 // The Content-Length value will not be reported as different if at either side
262 // (the data reduction proxy side and the client side), the Content-Length is
263 // missing or it cannot be decoded as a valid integer.
264 bool DataReductionProxyTamperDetection::ValidateContentLengthHeader(
265 const std::string
& fingerprint
) const {
266 int received_content_length_fingerprint
, actual_content_length
;
267 // Abort, if Content-Length value from the data reduction proxy does not
268 // exist or it cannot be converted to an integer.
269 if (!base::StringToInt(fingerprint
, &received_content_length_fingerprint
))
272 std::string actual_content_length_string
;
273 // Abort, if there is no Content-Length header received.
274 if (!response_headers_
->GetNormalizedHeader("Content-Length",
275 &actual_content_length_string
)) {
279 // Abort, if the Content-Length value cannot be converted to integer.
280 if (!base::StringToInt(actual_content_length_string
,
281 &actual_content_length
)) {
285 return received_content_length_fingerprint
!= actual_content_length
;
288 void DataReductionProxyTamperDetection::
289 ReportUMAforContentLengthHeaderValidation() const {
290 // Gets MIME type of the response and reports to UMA histograms separately.
291 // Divides MIME types into 4 groups: JavaScript, CSS, Images, and others.
292 REPORT_TAMPER_DETECTION_UMA(
294 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength",
295 "DataReductionProxy.HeaderTamperedHTTP_ContentLength",
299 std::string mime_type
;
300 response_headers_
->GetMimeType(&mime_type
);
302 std::string JS1
= "text/javascript";
303 std::string JS2
= "application/x-javascript";
304 std::string JS3
= "application/javascript";
305 std::string CSS
= "text/css";
306 std::string IMAGE
= "image/";
308 size_t mime_type_size
= mime_type
.size();
309 if ((mime_type_size
>= JS1
.size() && LowerCaseEqualsASCII(mime_type
.begin(),
310 mime_type
.begin() + JS1
.size(), JS1
.c_str())) ||
311 (mime_type_size
>= JS2
.size() && LowerCaseEqualsASCII(mime_type
.begin(),
312 mime_type
.begin() + JS2
.size(), JS2
.c_str())) ||
313 (mime_type_size
>= JS3
.size() && LowerCaseEqualsASCII(mime_type
.begin(),
314 mime_type
.begin() + JS3
.size(), JS3
.c_str()))) {
315 REPORT_TAMPER_DETECTION_UMA(
317 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength_JS",
318 "DataReductionProxy.HeaderTamperedHTTP_ContentLength_JS",
320 } else if (mime_type_size
>= CSS
.size() &&
321 LowerCaseEqualsASCII(mime_type
.begin(),
322 mime_type
.begin() + CSS
.size(), CSS
.c_str())) {
323 REPORT_TAMPER_DETECTION_UMA(
325 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength_CSS",
326 "DataReductionProxy.HeaderTamperedHTTP_ContentLength_CSS",
328 } else if (mime_type_size
>= IMAGE
.size() &&
329 LowerCaseEqualsASCII(mime_type
.begin(),
330 mime_type
.begin() + IMAGE
.size(), IMAGE
.c_str())) {
331 REPORT_TAMPER_DETECTION_UMA(
333 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength_Image",
334 "DataReductionProxy.HeaderTamperedHTTP_ContentLength_Image",
337 REPORT_TAMPER_DETECTION_UMA(
339 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength_Other",
340 "DataReductionProxy.HeaderTamperedHTTP_ContentLength_Other",
345 // We construct a canonical representation of the header so that reordered
346 // header values will produce the same fingerprint. The fingerprint is
347 // constructed as follows:
348 // 1) sort the values;
349 // 2) concatenate sorted values with a "," delimiter.
350 std::string
DataReductionProxyTamperDetection::ValuesToSortedString(
351 std::vector
<std::string
>* values
) {
352 std::string concatenated_values
;
354 if (!values
) return "";
356 std::sort(values
->begin(), values
->end());
357 for (size_t i
= 0; i
< values
->size(); ++i
) {
358 // Concatenates with delimiter ",".
359 concatenated_values
+= (*values
)[i
] + ",";
361 return concatenated_values
;
364 void DataReductionProxyTamperDetection::GetMD5(
365 const std::string
& input
, std::string
* output
) {
366 base::MD5Digest digest
;
367 base::MD5Sum(input
.c_str(), input
.size(), &digest
);
368 *output
= std::string(
369 reinterpret_cast<char*>(digest
.a
), ARRAYSIZE_UNSAFE(digest
.a
));
372 std::vector
<std::string
> DataReductionProxyTamperDetection::GetHeaderValues(
373 const net::HttpResponseHeaders
* headers
,
374 const std::string
& header_name
) {
375 std::vector
<std::string
> values
;
378 while (headers
->EnumerateHeader(&iter
, header_name
, &value
)) {
379 values
.push_back(value
);
384 } // namespace data_reduction_proxy