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