| Index: components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.cc
|
| diff --git a/components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.cc b/components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..6de4393f8f648f3d514c0e7c65a7f332e0c4c3cb
|
| --- /dev/null
|
| +++ b/components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.cc
|
| @@ -0,0 +1,432 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +// This file implements the tamper detection logic, where we want to detect
|
| +// whether there are middleboxes and whether they are tampering the response
|
| +// which maybe break correct communication and data transfer between Chrome
|
| +// and data reduction proxy.
|
| +//
|
| +// A high-level description of our tamper detection process works in two steps:
|
| +// 1. Data reduction proxy selects the requests we want to detect tamper;
|
| +// for the selected ones, data reduction proxy generates a series of
|
| +// fingerprints of the response, and append them to the Chrome-Proxy header;
|
| +// 2. At Chrome client side, once it sees such fingerprints, it uses the
|
| +// same method of data reduction proxy to generate the fingerprints on
|
| +// the response it receives, compare it to the result on the response
|
| +// data reduction proxy sends, i.e., the attached fingerprints in
|
| +// Chrome-Proxy header, to see if they are identical and report if there is
|
| +// any tamper detected to UMA.
|
| +//
|
| +// Right now we have 4 fingerprints. Chrome parses the fingerprints, check
|
| +// whether there is tampering on each of them, and report the result to UMA:
|
| +// 1. Chrome-Proxy header
|
| +// whether values of Chrome-Proxy have been tampered;
|
| +// 2. Via header
|
| +// whether there are middleboxes between Chrome and data reduction proxy;
|
| +// 3. Some other headers
|
| +// whether the values of a list of headers have been tampered;
|
| +// 4. Content-Length header
|
| +// whether the value of Content-Length is different to what data reduction
|
| +// proxy sends, which indicates that the response body has been tampered.
|
| +//
|
| +// Then Chrome reports tamper or not information to UMA.
|
| +// In general, Chrome reports the number of tampers for each fingerprint
|
| +// on different carriers, as well as total number of tamper detection handled.
|
| +// The only special case is the 4th one, Content-Length,
|
| +// which we have another dimension, MIME types, Chrome reports the tamper on
|
| +// different MIME type independently.
|
| +
|
| +
|
| +#include <string.h>
|
| +#include <algorithm>
|
| +#include <vector>
|
| +
|
| +#include "base/base64.h"
|
| +#include "base/md5.h"
|
| +#include "base/metrics/histogram.h"
|
| +#include "base/metrics/sparse_histogram.h"
|
| +#include "base/strings/string_number_conversions.h"
|
| +#include "components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detect.h"
|
| +
|
| +#include "net/android/network_library.h"
|
| +#include "net/http/http_request_headers.h"
|
| +#include "net/http/http_util.h"
|
| +
|
| +// Macro for UMA report.
|
| +// If |scheme_is_https| is true, report to |https_histogram|,
|
| +// otherwise report to |http_histogram|.
|
| +// Both's bucket are Carrier IDs |mcc_mnc|.
|
| +// The other histogram counts the total number, |http(s)_histogram| "_Total".
|
| +// which only has one bucket, 0.
|
| +#define UMA_REPORT(scheme_is_https, http_histogram, https_histogram, mcc_mnc) \
|
| + do { \
|
| + if (scheme_is_https) { \
|
| + UMA_HISTOGRAM_SPARSE_SLOWLY(https_histogram, mcc_mnc); \
|
| + UMA_HISTOGRAM_COUNTS(https_histogram "_Total", 1); \
|
| + } else { \
|
| + UMA_HISTOGRAM_SPARSE_SLOWLY(http_histogram, mcc_mnc); \
|
| + UMA_HISTOGRAM_COUNTS(http_histogram "_Total", 1); \
|
| + }\
|
| + } while (0)
|
| +
|
| +
|
| +
|
| +
|
| +
|
| +namespace data_reduction_proxy {
|
| +
|
| +const char kTamperDetectFingerprint[] = "fp=";
|
| +const char kTamperDetectFingerprintChromeProxy[] = "cp=";
|
| +
|
| +// Fingerprint |kTamperDetectFingerprint| contains multiple
|
| +// fingerprints, each starts with a tag followed by "=" and its fingerprint
|
| +// value. Three fingerprints and their respective tags are defined below.
|
| +const char kTamperDetectFingerprintVia[] = "via";
|
| +const char kTamperDetectFingerprintOther[] = "oh";
|
| +const char kTamperDetectFingerprintContengLength[] = "cl";
|
| +
|
| +// Utility function, exposed for unittest.
|
| +// Enumerate the values of Chrome-Proxy header and check if there is the
|
| +// fingerprint of Chrome-Proxy header (kTamperDetectFingerprintChromeProxy)
|
| +// and other fingerprints (kTamperDetectFingerprint)
|
| +bool ContainsTamperDetectFingerprints(std::vector<std::string>& values,
|
| + std::string& chrome_proxy_fingerprint,
|
| + std::string& other_fingerprints) {
|
| + bool contains_tamper_detect_fingerprints = false;
|
| + for (size_t i = 0; i < values.size(); ++i) {
|
| + if (values[i].find(data_reduction_proxy::
|
| + kTamperDetectFingerprintChromeProxy) == 0) {
|
| + contains_tamper_detect_fingerprints = true;
|
| + // Save Chrome-Proxy fingerprint.
|
| + chrome_proxy_fingerprint = values[i].substr(strlen(data_reduction_proxy::
|
| + kTamperDetectFingerprintChromeProxy));
|
| + // Erase Chrome-Proxy's fingerprint from Chrome-Proxy header for
|
| + // later fingerprint calculation.
|
| + values.erase(values.begin() + i);
|
| + // Adjust index |i| after erasing.
|
| + --i;
|
| + }
|
| + else if (values[i].find(data_reduction_proxy::kTamperDetectFingerprint)
|
| + == 0) {
|
| + // Save other fingerprints.
|
| + other_fingerprints = values[i].substr(strlen(data_reduction_proxy::
|
| + kTamperDetectFingerprint));
|
| + }
|
| + }
|
| + return contains_tamper_detect_fingerprints;
|
| +}
|
| +
|
| +void CheckResponseFingerprint(const net::HttpResponseHeaders* headers,
|
| + const bool is_secure_scheme)
|
| +{
|
| + std::vector<std::string> values = DataReductionProxyTamperDetect::
|
| + GetHeaderValues(headers, "Chrome-Proxy");
|
| +
|
| + // |chrome_proxy_fingerprint| holds the value of fingerprint of
|
| + // Chrome-Proxy header.
|
| + // |other_fingerprints| holds the value of other fingerprints.
|
| + std::string chrome_proxy_fingerprint, other_fingerprints;
|
| +
|
| + // Check if there are fingerprints (and thus need to detect tamper).
|
| + if (!ContainsTamperDetectFingerprints(values,
|
| + chrome_proxy_fingerprint,
|
| + other_fingerprints))
|
| + return;
|
| +
|
| + // Found tamper detect request field.
|
| + // Get carrier ID.
|
| + unsigned mcc_mnc = 0;
|
| + base::StringToUint(net::android::GetTelephonyNetworkOperator(), &mcc_mnc);
|
| +
|
| + DataReductionProxyTamperDetect tamper_detect(headers, is_secure_scheme,
|
| + mcc_mnc, &values);
|
| +
|
| + // Check if Chrome-Proxy header has been tampered.
|
| + if (tamper_detect.CheckHeaderChromeProxy(chrome_proxy_fingerprint)) {
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTampered_ChromeProxy",
|
| + "DataReductionProxy.HTTPHeaderTampered_ChromeProxy",
|
| + mcc_mnc);
|
| + return;
|
| + } else
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTamperDetection",
|
| + "DataReductionProxy.HTTPHeaderTamperDetection",
|
| + mcc_mnc);
|
| +
|
| + // Separate fingerprints from |other_fingerprints|.
|
| + net::HttpUtil::ValuesIterator it(other_fingerprints.begin(),
|
| + other_fingerprints.end(), '|');
|
| +
|
| + // For each fingerprint, get its name |key| and the fingerprint value |value|
|
| + // from data reduction proxy. CheckReportFingerprint will handle the tamper
|
| + // detect and corresponding UMA report.
|
| + size_t delimiter_pos = std::string::npos;
|
| + while (it.GetNext()) {
|
| +
|
| + delimiter_pos = it.value().find("=");
|
| + if (delimiter_pos == std::string::npos)
|
| + continue;
|
| + std::string key = it.value().substr(0, delimiter_pos);
|
| + std::string value = it.value().substr(delimiter_pos + 1);
|
| +
|
| + FingerprintCode fingerprint_code = tamper_detect.GetFingerprintCode(key);
|
| + switch (fingerprint_code) {
|
| + case VIA:
|
| + if (tamper_detect.CheckHeaderVia(value))
|
| + tamper_detect.ReportHeaderVia();
|
| + break;
|
| + case OTHERHEADERS:
|
| + if (tamper_detect.CheckHeaderOtherHeaders(value))
|
| + tamper_detect.ReportHeaderOtherHeaders();
|
| + break;
|
| + case CONTENTLENGTH:
|
| + if (tamper_detect.CheckHeaderContentLength(value))
|
| + tamper_detect.ReportHeaderContentLength();
|
| + break;
|
| + case CHROMEPROXY:
|
| + case NONEXIST:
|
| + break;
|
| + }
|
| + }
|
| + return;
|
| +}
|
| +
|
| +// It initialize the fingerprint tag to fingerprint code map.
|
| +// Right now we have 3 fingerprints to check (besides Chrome-Proxy header's
|
| +// fingerprint, which has been handled specially.
|
| +DataReductionProxyTamperDetect::DataReductionProxyTamperDetect(
|
| + const net::HttpResponseHeaders* headers, const bool secure,
|
| + const unsigned mcc_mnc_, std::vector<std::string>* values)
|
| + : response_headers(headers),
|
| + is_secure_scheme(secure),
|
| + mcc_mnc(mcc_mnc_),
|
| + clean_chrome_proxy_header_values(values) {
|
| + fingperprint_tag_code_map = std::map<std::string, FingerprintCode>();
|
| + fingperprint_tag_code_map[kTamperDetectFingerprintVia] = VIA;
|
| + fingperprint_tag_code_map[kTamperDetectFingerprintOther] = OTHERHEADERS;
|
| + fingperprint_tag_code_map[kTamperDetectFingerprintContengLength] =
|
| + CONTENTLENGTH;
|
| +};
|
| +
|
| +DataReductionProxyTamperDetect::~DataReductionProxyTamperDetect() {};
|
| +
|
| +
|
| +// Utility function...
|
| +// Sort the strings in |values| alphabetically, concatenate them into a string.
|
| +std::string DataReductionProxyTamperDetect::ValuesToSortedString(
|
| + std::vector<std::string> &values) {
|
| + std::string aggregated_values;
|
| +
|
| + std::sort(values.begin(), values.end());
|
| + for (size_t i = 0; i < values.size(); ++i)
|
| + aggregated_values += values[i] + ",";
|
| + return aggregated_values;
|
| +}
|
| +
|
| +// Utility function...
|
| +// For a given string, calculate and return the MD5 hash value of the string.
|
| +std::string DataReductionProxyTamperDetect::GetMD5(const std::string &input) {
|
| + base::MD5Context context;
|
| + base::MD5Init(&context);
|
| + base::MD5Update(&context, input);
|
| + base::MD5Digest new_digest;
|
| + base::MD5Final(&new_digest, &context);
|
| + return std::string((char*)new_digest.a, ARRAYSIZE_UNSAFE(new_digest.a));
|
| +}
|
| +
|
| +// Utility function...
|
| +// For a given |header_name|, get all its values and return the vector contains
|
| +// all of the values.
|
| +std::vector<std::string> DataReductionProxyTamperDetect::GetHeaderValues(
|
| + const net::HttpResponseHeaders* headers, const std::string& header_name) {
|
| + std::vector<std::string> values;
|
| + std::string value;
|
| + void* iter = NULL;
|
| + while (headers->EnumerateHeader(&iter, header_name, &value)) {
|
| + values.push_back(value);
|
| + }
|
| + return values;
|
| +}
|
| +
|
| +FingerprintCode DataReductionProxyTamperDetect::GetFingerprintCode(
|
| + const std::string& tag) {
|
| + std::map<std::string, FingerprintCode>::iterator it =
|
| + fingperprint_tag_code_map.find(tag);
|
| +
|
| + return it == fingperprint_tag_code_map.end() ? NONEXIST : it->second;
|
| +}
|
| +
|
| +
|
| +
|
| +// Check whether Chrome-Proxy header has been tampered.
|
| +// |fingerprint| is the fingerprint Chrome received from data reduction proxy,
|
| +// which is Base64 encoded. Decode it first. Calculate the hash value of
|
| +// Chrome-Proxy header. Note that |clean_chrome_proxy_header_values| holds
|
| +// the values of Chrome-Proxy header with its own fingerprint removed,
|
| +// so it's the correct values to be used to calculate fingerprint.
|
| +// Compare calculated fingerprint to the fingerprint from data reduction proxy
|
| +// (the removed value) and see there is tamper detected.
|
| +bool DataReductionProxyTamperDetect::CheckHeaderChromeProxy(
|
| + const std::string& fingerprint) const {
|
| + std::string received_fingerprint;
|
| + if (!base::Base64Decode(fingerprint, &received_fingerprint))
|
| + return false;
|
| + // Calculate the MD5 hash value of Chrome-Proxy.
|
| + std::string actual_fingerprint = GetMD5(
|
| + ValuesToSortedString(*clean_chrome_proxy_header_values));
|
| +
|
| + // Compare and check if there is tamper detected.
|
| + return received_fingerprint != actual_fingerprint;
|
| +}
|
| +
|
| +// For Via header tamper detection...
|
| +// Check whether there are proxies/middleboxes between Chrome
|
| +// and data reduction proxy. Concretely, it checks whether there are other
|
| +// proxies/middleboxes' name after data reduction proxy's name in Via header.
|
| +bool DataReductionProxyTamperDetect::CheckHeaderVia(
|
| + const std::string& fingerprint) const {
|
| +
|
| + std::vector<std::string> vias = GetHeaderValues(response_headers, "via");
|
| +
|
| + // If there is no tag, then data reduction proxy's tag have been removed.
|
| + if (vias.size() == 0) return true;
|
| + // Check whether the last proxy/middlebox is data reduction proxy or not.
|
| +
|
| + bool seen_data_reduction_proxy = false;
|
| + // change 2 to constant ... figure it out how to do it
|
| + for (int i = 0; i < 2; ++i) {
|
| + if (vias[vias.size() - 1].find(kDataReductionProxyViaValues[i]) !=
|
| + std::string::npos) {
|
| + seen_data_reduction_proxy = true;
|
| + break;
|
| + }
|
| + }
|
| +
|
| + return !seen_data_reduction_proxy;
|
| +}
|
| +
|
| +// Report Via header tamper detected.
|
| +void DataReductionProxyTamperDetect::ReportHeaderVia() const {
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTampered_Via",
|
| + "DataReductionProxy.HTTPHeaderTampered_Via",
|
| + mcc_mnc);
|
| +}
|
| +
|
| +
|
| +// For other headers tamper detection...
|
| +// Check whether values of a predefined list of headers have been tampered.
|
| +// The format for |fingerprint| is:
|
| +// [base64fingerprint]:header_name1:header_namer2:...
|
| +// Firstly extract the header names in the |fingerprint|.
|
| +// For each header,
|
| +// 1) we get all the values of such header;
|
| +// 2) we sort the values alphabetically;
|
| +// 3) we concatenate sorted values to a string and calculate MD5 hash on it.
|
| +// Finally, we compare whether it equals to the fingerprint from
|
| +// data reduction proxy.
|
| +bool DataReductionProxyTamperDetect::CheckHeaderOtherHeaders(
|
| + const std::string& fingerprint) const {
|
| + std::string received_fingerprint;
|
| +
|
| + net::HttpUtil::ValuesIterator it(fingerprint.begin(),
|
| + fingerprint.end(), ':');
|
| +
|
| + // Make sure there is [base64fingerprint] and it can be decoded.
|
| + if (!(it.GetNext() &&
|
| + base::Base64Decode(std::string(it.value()), &received_fingerprint)))
|
| + return false;
|
| +
|
| + std::string header_values;
|
| + // Enumerate the list of headers.
|
| + while (it.GetNext()) {
|
| + // Get values of one header.
|
| + std::vector<std::string> values =
|
| + GetHeaderValues(response_headers, std::string(it.value()));
|
| + // Sort the values and concatenate them.
|
| + header_values += ValuesToSortedString(values) + ";";
|
| + }
|
| +
|
| + // Calculate MD5 hash value on the concatenated string.
|
| + std::string actual_fingerprint = GetMD5(header_values);
|
| +
|
| + return received_fingerprint != actual_fingerprint;
|
| +}
|
| +
|
| +// Report other headers tamper detected.
|
| +void DataReductionProxyTamperDetect::ReportHeaderOtherHeaders() const {
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTampered_OtherHeaders",
|
| + "DataReductionProxy.HTTPHeaderTampered_OtherHeaders",
|
| + mcc_mnc);
|
| +}
|
| +
|
| +
|
| +// For Content-Length tamper detection...
|
| +// Check whether the Content-Length value is different from what
|
| +// data reduction proxy sees. This is an indicator that the response body
|
| +// have been modified.
|
| +// It's modified only if we can decode Content-Length numbers at both end
|
| +// and such two numbers are not equal.
|
| +bool DataReductionProxyTamperDetect::CheckHeaderContentLength(
|
| + const std::string& fingerprint) const {
|
| + int received_content_length, actual_content_length;
|
| + // If Content-Length value from data reduction proxy is not available or
|
| + // it cannot be converted to integer, pass.
|
| + if (base::StringToInt(fingerprint, &received_content_length)) {
|
| + std::string actual_content_length_;
|
| + // If there is Content-Length at Chrome client side is not available, pass.
|
| + if (response_headers->GetNormalizedHeader("Content-Length",
|
| + &actual_content_length_)) {
|
| + // If the Content-Length value cannot be converted to integer,
|
| + // i.e., not valid, pass.
|
| + if (!base::StringToInt(actual_content_length_, &actual_content_length))
|
| + return false;
|
| + return received_content_length != actual_content_length;
|
| + }
|
| + }
|
| + return false;
|
| +}
|
| +
|
| +// Report Content-Length tamper detected.
|
| +// Get MIME type of the response and report to different UMA histogram.
|
| +// Right now MIME types contain JavaScript, CSS, Images, and others.
|
| +void DataReductionProxyTamperDetect::ReportHeaderContentLength() const {
|
| + std::string mime_type;
|
| + // Get MIME type.
|
| + response_headers->GetMimeType(&mime_type);
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTampered_ContentLength",
|
| + "DataReductionProxy.HTTPHeaderTampered_ContentLength",
|
| + mcc_mnc);
|
| +
|
| + // Report tampered JavaScript.
|
| + if (mime_type.compare("text/javascript") == 0 ||
|
| + mime_type.compare("application/x-javascript") == 0 ||
|
| + mime_type.compare("application/javascript") == 0)
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTampered_ContentLength_JS",
|
| + "DataReductionProxy.HTTPHeaderTampered_ContentLength_JS",
|
| + mcc_mnc);
|
| + // Report tampered CSSs.
|
| + else if (mime_type.compare("text/css") == 0)
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTampered_ContentLength_CSS",
|
| + "DataReductionProxy.HTTPHeaderTampered_ContentLength_CSS",
|
| + mcc_mnc);
|
| + // Report tampered images.
|
| + else if (mime_type.find("image") == 0)
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTampered_ContentLength_Image",
|
| + "DataReductionProxy.HTTPHeaderTampered_ContentLength_Image",
|
| + mcc_mnc);
|
| + // Report tampered other MIME types.
|
| + else
|
| + UMA_REPORT(is_secure_scheme,
|
| + "DataReductionProxy.HTTPSHeaderTampered_ContentLength_Other",
|
| + "DataReductionProxy.HTTPHeaderTampered_ContentLength_Other",
|
| + mcc_mnc);
|
| +}
|
| +} // namespace data_reduction_proxy
|
|
|