OLD | NEW |
---|---|
(Empty) | |
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. | |
4 | |
5 // This file implements the tamper detection logic, where we want to detect | |
6 // whether there are middleboxes and whether they are tampering the response | |
7 // which maybe break correct communication and data transfer between Chrome | |
8 // and data reduction proxy. | |
9 // | |
10 // A high-level description of our tamper detection process works in two steps: | |
11 // 1. Data reduction proxy selects the requests we want to detect tamper; | |
12 // for the selected ones, data reduction proxy generates a series of | |
13 // fingerprints of the response, and append them to the Chrome-Proxy header; | |
14 // 2. At Chrome client side, once it sees such fingerprints, it uses the | |
15 // same method of data reduction proxy to generate the fingerprints on | |
16 // the response it receives, compare it to the result on the response | |
17 // data reduction proxy sends, i.e., the attached fingerprints in | |
18 // Chrome-Proxy header, to see if they are identical and report to UMA if | |
19 // there is any tamper detected. | |
20 // | |
21 // Right now we have 4 fingerprints (listed below). Chrome first check the | |
22 // fingerprint of Chrome-Proxy header. If Chrome-Proxy header has been | |
23 // tampered, then other fingerprints would not be checked; if not, Chrome | |
24 // parses the rest of the fingerprints and check whether there is tampering | |
25 // on each of them. | |
26 // | |
27 // 1. Chrome-Proxy header | |
28 // whether values of Chrome-Proxy have been tampered; | |
29 // 2. Via header | |
30 // whether there are middleboxes between Chrome and data reduction proxy; | |
31 // 3. Some other headers | |
32 // whether the values of a list of headers have been tampered; | |
33 // 4. Content-Length header | |
34 // whether the value of Content-Length is different to what data reduction | |
35 // proxy sends, which indicates that the response body has been tampered. | |
36 // | |
37 // Chrome reports tamper or not information for each fingerprint to UMA. In | |
38 // general, Chrome reports the number of tampers for each fingerprint on | |
39 // different carriers, as well as total number of tamper detection handled. | |
40 // The only special case is the 4th fingerprint, Content-Length, which we have | |
41 // another dimension, MIME types, Chrome reports the tamper on different MIME | |
42 // type independently. | |
43 | |
44 | |
45 #include <string.h> | |
46 #include <algorithm> | |
47 #include <vector> | |
48 | |
49 #include "base/base64.h" | |
50 #include "base/md5.h" | |
51 #include "base/metrics/histogram.h" | |
52 #include "base/metrics/sparse_histogram.h" | |
53 #include "base/strings/string_number_conversions.h" | |
54 #include "components/data_reduction_proxy/browser/data_reduction_proxy_tamper_de tect.h" | |
55 | |
56 #include "net/android/network_library.h" | |
57 #include "net/http/http_request_headers.h" | |
58 #include "net/http/http_util.h" | |
59 | |
60 // Macro for UMA report. First report to either |https_histogram| or | |
61 // |http_histogram| depends on |scheme_is_https|, with carrier ID as bucket | |
62 // |mcc_mnc|. Then report to http(s)_histogram_Total, which counts the total | |
63 // number. | |
64 #define UMA_REPORT(scheme_is_https, http_histogram, https_histogram, mcc_mnc) \ | |
65 do { \ | |
66 if (scheme_is_https) { \ | |
67 UMA_HISTOGRAM_SPARSE_SLOWLY(https_histogram, mcc_mnc); \ | |
68 UMA_HISTOGRAM_COUNTS(https_histogram "_Total", 1); \ | |
69 } else { \ | |
70 UMA_HISTOGRAM_SPARSE_SLOWLY(http_histogram, mcc_mnc); \ | |
71 UMA_HISTOGRAM_COUNTS(http_histogram "_Total", 1); \ | |
72 }\ | |
73 } while (0) | |
74 | |
75 namespace data_reduction_proxy { | |
76 | |
77 namespace { | |
78 // Two fingerprints will be added to Chrome-Proxy header. One starts with | |
79 // |kTamperDetectFingerprintChromeProxy|, which is the fingerprint for the | |
80 // Chrome-Proxy header. The other one starts with |kTamperDetectFingerprint|, | |
81 // which includes all other fingerprints. | |
82 const char kTamperDetectFingerprint[] = "fp="; | |
83 const char kTamperDetectFingerprintChromeProxy[] = "cp="; | |
84 | |
85 // Fingerprint |kTamperDetectFingerprint| contains multiple | |
86 // fingerprints, each starts with a tag followed by "=" and its fingerprint | |
87 // value. Three fingerprints and their respective tags are defined below. | |
88 const char kTamperDetectFingerprintVia[] = "via"; | |
89 const char kTamperDetectFingerprintOther[] = "oh"; | |
90 const char kTamperDetectFingerprintContengLength[] = "cl"; | |
91 | |
92 } // namespace | |
93 | |
94 void DataReductionProxyTamperDetect::CheckResponseFingerprint( | |
95 const net::HttpResponseHeaders* headers, | |
96 const bool is_secure_scheme) | |
97 { | |
98 std::vector<std::string> values = DataReductionProxyTamperDetect:: | |
99 GetHeaderValues(headers, "Chrome-Proxy"); | |
100 | |
101 // |chrome_proxy_fingerprint| holds the value of fingerprint of | |
102 // Chrome-Proxy header. | |
103 // |other_fingerprints| holds the value of other fingerprints. | |
104 std::string chrome_proxy_fingerprint, other_fingerprints; | |
105 | |
106 // Check if there are fingerprints (and thus need to detect tamper). | |
107 if (!ContainsTamperDetectFingerprints(&values, | |
108 &chrome_proxy_fingerprint, | |
109 &other_fingerprints)) | |
110 return; | |
111 | |
112 // Found tamper detect request field. | |
113 // Get carrier ID. | |
114 unsigned mcc_mnc = 0; | |
115 base::StringToUint(net::android::GetTelephonyNetworkOperator(), &mcc_mnc); | |
116 | |
117 DataReductionProxyTamperDetect tamper_detect(headers, is_secure_scheme, | |
118 mcc_mnc, &values); | |
119 | |
120 // Check if Chrome-Proxy header has been tampered. | |
121 if (tamper_detect.IsChromeProxyHeaderTampered(chrome_proxy_fingerprint)) { | |
122 UMA_REPORT(is_secure_scheme, | |
123 "DataReductionProxy.HTTPSHeaderTampered_ChromeProxy", | |
124 "DataReductionProxy.HTTPHeaderTampered_ChromeProxy", | |
125 mcc_mnc); | |
126 return; | |
127 } else | |
128 UMA_REPORT(is_secure_scheme, | |
129 "DataReductionProxy.HTTPSHeaderTamperDetection", | |
130 "DataReductionProxy.HTTPHeaderTamperDetection", | |
131 mcc_mnc); | |
132 | |
133 // Separate fingerprints from |other_fingerprints|. | |
134 net::HttpUtil::ValuesIterator it(other_fingerprints.begin(), | |
135 other_fingerprints.end(), '|'); | |
136 | |
137 // For each fingerprint, get its name |key| and the fingerprint value |value| | |
138 // from data reduction proxy. CheckReportFingerprint will handle the tamper | |
139 // detect and corresponding UMA report. | |
140 size_t delimiter_pos = std::string::npos; | |
141 while (it.GetNext()) { | |
142 delimiter_pos = it.value().find("="); | |
143 if (delimiter_pos == std::string::npos) | |
144 continue; | |
145 std::string key = it.value().substr(0, delimiter_pos); | |
146 std::string value = it.value().substr(delimiter_pos + 1); | |
147 | |
148 FingerprintCode fingerprint_code = tamper_detect.GetFingerprintCode(key); | |
149 switch (fingerprint_code) { | |
150 case VIA: | |
151 if (tamper_detect.IsViaHeaderTampered(value)) | |
152 tamper_detect.ReportViaHeaderTamperedUMA(); | |
153 break; | |
154 case OTHERHEADERS: | |
155 if (tamper_detect.AreOtherHeadersTampered(value)) | |
156 tamper_detect.ReportOtherHeadersTamperedUMA(); | |
157 break; | |
158 case CONTENTLENGTH: | |
159 if (tamper_detect.IsContentLengthHeaderTampered(value)) | |
160 tamper_detect.ReportContentLengthHeaderTamperedUMA(); | |
161 break; | |
162 case CHROMEPROXY: | |
bolian
2014/07/09 22:51:06
Replace the last two cases with
case default:
xingx
2014/07/10 03:07:37
Done.
| |
163 case NONEXIST: | |
164 break; | |
165 } | |
166 } | |
167 return; | |
168 } | |
169 | |
170 // Initialize fingerprint code map. | |
171 DataReductionProxyTamperDetect::DataReductionProxyTamperDetect( | |
172 const net::HttpResponseHeaders* headers, const bool secure, | |
173 const unsigned mcc_mnc_, std::vector<std::string>* values) | |
174 : response_headers(headers), | |
175 is_secure_scheme(secure), | |
176 mcc_mnc(mcc_mnc_), | |
177 clean_chrome_proxy_header_values(values) { | |
178 fingperprint_tag_code_map = std::map<std::string, FingerprintCode>(); | |
179 fingperprint_tag_code_map[kTamperDetectFingerprintVia] = VIA; | |
180 fingperprint_tag_code_map[kTamperDetectFingerprintOther] = OTHERHEADERS; | |
181 fingperprint_tag_code_map[kTamperDetectFingerprintContengLength] = | |
182 CONTENTLENGTH; | |
183 }; | |
184 | |
185 DataReductionProxyTamperDetect::~DataReductionProxyTamperDetect() {}; | |
186 | |
187 // Check whether Chrome-Proxy header has been tampered. | |
188 // |fingerprint| is the fingerprint Chrome received from data reduction proxy, | |
189 // which is Base64 encoded. Decode it first. Calculate the hash value of | |
190 // Chrome-Proxy header. Note that |clean_chrome_proxy_header_values| holds | |
191 // the values of Chrome-Proxy header with its own fingerprint removed, | |
192 // so it's the correct values to be used to calculate fingerprint. | |
193 // Compare calculated fingerprint to the fingerprint from data reduction proxy | |
194 // (the removed value) and see there is tamper detected. | |
195 bool DataReductionProxyTamperDetect::IsChromeProxyHeaderTampered( | |
196 const std::string& fingerprint) const { | |
197 std::string received_fingerprint; | |
198 if (!base::Base64Decode(fingerprint, &received_fingerprint)) | |
bolian
2014/07/09 22:51:05
This should be treated as tampered, right?
xingx
2014/07/10 03:07:40
That's right, my mistake.
| |
199 return false; | |
200 // Calculate the MD5 hash value of Chrome-Proxy. | |
201 std::string actual_fingerprint = GetMD5( | |
202 ValuesToSortedString(*clean_chrome_proxy_header_values)); | |
203 | |
204 return received_fingerprint != actual_fingerprint; | |
205 } | |
206 | |
207 // Check whether there are proxies/middleboxes between Chrome | |
208 // and data reduction proxy. Concretely, it checks whether there are other | |
209 // proxies/middleboxes' name after data reduction proxy's name in Via header. | |
210 bool DataReductionProxyTamperDetect::IsViaHeaderTampered( | |
211 const std::string& fingerprint) const { | |
212 | |
213 std::vector<std::string> vias = GetHeaderValues(response_headers, "via"); | |
214 | |
215 // If there is no tag, then data reduction proxy's tag have been removed. | |
216 if (vias.size() == 0) return true; | |
217 // Check whether the last proxy/middlebox is data reduction proxy or not. | |
218 | |
219 bool seen_data_reduction_proxy = false; | |
220 // TODO(xingx): change 2 to array size of such constant array. | |
bolian
2014/07/09 22:51:06
why not do it now?
xingx
2014/07/10 03:07:38
Need to figure it out...
| |
221 for (int i = 0; i < 2; ++i) { | |
222 if (vias[vias.size() - 1].find(kDataReductionProxyViaValues[i]) != | |
223 std::string::npos) { | |
224 seen_data_reduction_proxy = true; | |
225 break; | |
226 } | |
227 } | |
228 | |
229 return !seen_data_reduction_proxy; | |
230 } | |
231 | |
232 // Report Via header tamper detected. | |
233 void DataReductionProxyTamperDetect::ReportViaHeaderTamperedUMA() const { | |
234 UMA_REPORT(is_secure_scheme, | |
235 "DataReductionProxy.HTTPSHeaderTampered_Via", | |
236 "DataReductionProxy.HTTPHeaderTampered_Via", | |
237 mcc_mnc); | |
238 } | |
239 | |
240 // Check whether values of a predefined list of headers have been tampered. | |
241 // The format for |fingerprint| is: | |
242 // [base64fingerprint]:header_name1:header_namer2:... | |
243 // Firstly extract the header names in the |fingerprint|. | |
244 // For each header, | |
245 // 1) get all the values of such header; | |
246 // 2) sort the values alphabetically; | |
247 // 3) concatenate sorted values to a string and calculate MD5 hash on it. | |
248 // Finally, compare whether it equals to the received fingerprint. | |
249 bool DataReductionProxyTamperDetect::AreOtherHeadersTampered( | |
250 const std::string& fingerprint) const { | |
251 std::string received_fingerprint; | |
252 | |
253 net::HttpUtil::ValuesIterator it(fingerprint.begin(), | |
254 fingerprint.end(), ':'); | |
255 | |
256 // Make sure there is [base64fingerprint] and it can be decoded. | |
257 if (!(it.GetNext() && | |
bolian
2014/07/09 22:51:06
Will the server always send "oh=" even if the head
xingx
2014/07/10 03:07:37
Yes.
Done.
| |
258 base::Base64Decode(std::string(it.value()), &received_fingerprint))) | |
259 return false; | |
260 | |
261 std::string header_values; | |
262 // Enumerate the list of headers. | |
263 while (it.GetNext()) { | |
264 // Get values of one header. | |
265 std::vector<std::string> values = | |
266 GetHeaderValues(response_headers, std::string(it.value())); | |
267 // Sort the values and concatenate them. | |
268 header_values += ValuesToSortedString(values) + ";"; | |
269 } | |
270 | |
271 // Calculate MD5 hash value on the concatenated string. | |
272 std::string actual_fingerprint = GetMD5(header_values); | |
273 | |
274 return received_fingerprint != actual_fingerprint; | |
275 } | |
276 | |
277 // Report other headers tamper detected. | |
278 void DataReductionProxyTamperDetect::ReportOtherHeadersTamperedUMA() const { | |
279 UMA_REPORT(is_secure_scheme, | |
280 "DataReductionProxy.HTTPSHeaderTampered_OtherHeaders", | |
281 "DataReductionProxy.HTTPHeaderTampered_OtherHeaders", | |
282 mcc_mnc); | |
283 } | |
284 | |
285 | |
286 // For Content-Length tamper detection... | |
287 // Check whether the Content-Length value is different from what | |
288 // data reduction proxy sees. This is an indicator that the response body | |
289 // have been modified. | |
290 // It's modified only if we can decode Content-Length numbers at both end | |
291 // and such two numbers are not equal. | |
292 bool DataReductionProxyTamperDetect::IsContentLengthHeaderTampered( | |
293 const std::string& fingerprint) const { | |
294 int received_content_length, actual_content_length; | |
295 // If Content-Length value from data reduction proxy is not available or | |
296 // it cannot be converted to integer, pass. | |
297 if (base::StringToInt(fingerprint, &received_content_length)) { | |
298 std::string actual_content_length_; | |
299 // If there is Content-Length at Chrome client side is not available, pass. | |
300 if (response_headers->GetNormalizedHeader("Content-Length", | |
301 &actual_content_length_)) { | |
302 // If the Content-Length value cannot be converted to integer, | |
303 // i.e., not valid, pass. | |
304 if (!base::StringToInt(actual_content_length_, &actual_content_length)) | |
305 return false; | |
306 return received_content_length != actual_content_length; | |
307 } | |
308 } | |
309 return false; | |
310 } | |
311 | |
312 // Report Content-Length tamper detected. | |
313 // Get MIME type of the response and report to different UMA histogram. | |
314 // Right now MIME types contain JavaScript, CSS, Images, and others. | |
315 void DataReductionProxyTamperDetect::ReportContentLengthHeaderTamperedUMA() | |
316 const { | |
317 UMA_REPORT(is_secure_scheme, | |
318 "DataReductionProxy.HTTPSHeaderTampered_ContentLength", | |
319 "DataReductionProxy.HTTPHeaderTampered_ContentLength", | |
320 mcc_mnc); | |
321 | |
322 // Get MIME type. | |
323 std::string mime_type; | |
324 response_headers->GetMimeType(&mime_type); | |
325 | |
326 | |
327 // Report tampered JavaScript. | |
328 if (mime_type.compare("text/javascript") == 0 || | |
329 mime_type.compare("application/x-javascript") == 0 || | |
330 mime_type.compare("application/javascript") == 0) | |
331 UMA_REPORT(is_secure_scheme, | |
332 "DataReductionProxy.HTTPSHeaderTampered_ContentLength_JS", | |
333 "DataReductionProxy.HTTPHeaderTampered_ContentLength_JS", | |
334 mcc_mnc); | |
335 // Report tampered CSSs. | |
336 else if (mime_type.compare("text/css") == 0) | |
337 UMA_REPORT(is_secure_scheme, | |
338 "DataReductionProxy.HTTPSHeaderTampered_ContentLength_CSS", | |
339 "DataReductionProxy.HTTPHeaderTampered_ContentLength_CSS", | |
340 mcc_mnc); | |
341 // Report tampered images. | |
342 else if (mime_type.find("image") == 0) | |
343 UMA_REPORT(is_secure_scheme, | |
344 "DataReductionProxy.HTTPSHeaderTampered_ContentLength_Image", | |
345 "DataReductionProxy.HTTPHeaderTampered_ContentLength_Image", | |
346 mcc_mnc); | |
347 // Report tampered other MIME types. | |
348 else | |
349 UMA_REPORT(is_secure_scheme, | |
350 "DataReductionProxy.HTTPSHeaderTampered_ContentLength_Other", | |
351 "DataReductionProxy.HTTPHeaderTampered_ContentLength_Other", | |
352 mcc_mnc); | |
353 } | |
354 | |
355 DataReductionProxyTamperDetect::FingerprintCode | |
356 DataReductionProxyTamperDetect::GetFingerprintCode( | |
357 const std::string& tag) { | |
358 std::map<std::string, DataReductionProxyTamperDetect::FingerprintCode> | |
359 ::iterator it = fingperprint_tag_code_map.find(tag); | |
360 | |
361 return it == fingperprint_tag_code_map.end() ? NONEXIST : it->second; | |
362 } | |
363 | |
364 // Enumerate the values of Chrome-Proxy header and check if there is the | |
365 // fingerprint of Chrome-Proxy header (kTamperDetectFingerprintChromeProxy) | |
366 // and other fingerprints (kTamperDetectFingerprint). If there are, save them | |
367 // accordingly. | |
368 bool DataReductionProxyTamperDetect::ContainsTamperDetectFingerprints( | |
bolian
2014/07/09 22:51:06
Rename this. The name sounds like it won't change
xingx
2014/07/10 03:07:40
Done.
| |
369 std::vector<std::string>* values, | |
370 std::string* chrome_proxy_fingerprint, | |
371 std::string* other_fingerprints) { | |
372 bool contains_tamper_detect_fingerprints = false; | |
373 for (size_t i = 0; i < values->size(); ++i) { | |
374 if ((*values)[i].find(data_reduction_proxy:: | |
bolian
2014/07/09 22:51:06
s/data_reduction_proxy:://
This is in the same nam
xingx
2014/07/10 03:07:37
Done.
| |
375 kTamperDetectFingerprintChromeProxy) == 0) { | |
376 contains_tamper_detect_fingerprints = true; | |
377 // Save Chrome-Proxy fingerprint. | |
378 *chrome_proxy_fingerprint = (*values)[i].substr(strlen( | |
379 data_reduction_proxy::kTamperDetectFingerprintChromeProxy)); | |
380 // Erase Chrome-Proxy's fingerprint from Chrome-Proxy header for | |
381 // later fingerprint calculation. | |
382 values->erase(values->begin() + i); | |
383 // Adjust index |i| after erasing. | |
384 --i; | |
bolian
2014/07/09 22:51:06
Don't change the vector while iterating. Do it out
xingx
2014/07/10 03:07:38
Done.
| |
385 } | |
386 else if ((*values)[i].find(data_reduction_proxy::kTamperDetectFingerprint) | |
387 == 0) { | |
388 // Save other fingerprints. | |
389 *other_fingerprints = (*values)[i].substr(strlen(data_reduction_proxy:: | |
390 kTamperDetectFingerprint)); | |
391 } | |
392 } | |
393 return contains_tamper_detect_fingerprints; | |
394 } | |
395 | |
396 // Utility function. Sort the strings in |values| alphabetically, concatenate | |
397 // them into a string. | |
398 std::string DataReductionProxyTamperDetect::ValuesToSortedString( | |
399 std::vector<std::string> &values) { | |
400 std::string aggregated_values; | |
401 | |
402 std::sort(values.begin(), values.end()); | |
403 for (size_t i = 0; i < values.size(); ++i) | |
404 aggregated_values += values[i] + ","; | |
405 return aggregated_values; | |
406 } | |
407 | |
408 // Utility function. For a given string, calculate and return the MD5 hash | |
409 // value of the string. | |
410 std::string DataReductionProxyTamperDetect::GetMD5(const std::string &input) { | |
411 base::MD5Digest digest; | |
412 base::MD5Sum(input.c_str(), input.size(), &digest); | |
413 return std::string((char*)digest.a, ARRAYSIZE_UNSAFE(digest.a)); | |
414 } | |
415 | |
416 // Utility function. For a given |header_name|, get all its values and return | |
417 // the vector contains all of the values. | |
418 std::vector<std::string> DataReductionProxyTamperDetect::GetHeaderValues( | |
419 const net::HttpResponseHeaders* headers, const std::string& header_name) { | |
420 std::vector<std::string> values; | |
421 std::string value; | |
422 void* iter = NULL; | |
423 while (headers->EnumerateHeader(&iter, header_name, &value)) { | |
424 values.push_back(value); | |
425 } | |
426 return values; | |
427 } | |
428 | |
429 } // namespace data_reduction_proxy | |
OLD | NEW |