Chromium Code Reviews| 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 |