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 #include "components/data_reduction_proxy/browser/data_reduction_proxy_tamper_detection.h" 6 7 #include <algorithm> 8 #include <cstring> 9 10 #include "base/base64.h" 11 #include "base/md5.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" 19 20 #if defined(OS_ANDROID) 21 #include "net/android/network_library.h" 22 #endif 23 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) \ 30 do { \ 31 if (scheme_is_https) { \ 32 UMA_HISTOGRAM_SPARSE_SLOWLY(https_histogram, carrier_id); \ 33 UMA_HISTOGRAM_COUNTS(https_histogram "_Total", 1); \ 34 } else { \ 35 UMA_HISTOGRAM_SPARSE_SLOWLY(http_histogram, carrier_id); \ 36 UMA_HISTOGRAM_COUNTS(http_histogram "_Total", 1); \ 37 }\ 38 } while (0) 39 40 namespace data_reduction_proxy { 41 42 // static 43 bool DataReductionProxyTamperDetection::DetectAndReport( 44 const net::HttpResponseHeaders* headers, 45 const bool scheme_is_https) { 46 DCHECK(headers); 47 // Abort tamper detection, if the fingerprint of the Chrome-Proxy header is 48 // absent. 49 std::string chrome_proxy_fingerprint; 50 if (!GetDataReductionProxyActionFingerprintChromeProxy( 51 headers, &chrome_proxy_fingerprint)) { 52 return false; 53 } 54 55 // Get carrier ID. 56 unsigned carrier_id = 0; 57 #if defined(OS_ANDROID) 58 base::StringToUint(net::android::GetTelephonyNetworkOperator(), &carrier_id); 59 #endif 60 61 DataReductionProxyTamperDetection tamper_detection( 62 headers, scheme_is_https, carrier_id); 63 64 // Checks if the Chrome-Proxy header has been tampered with. 65 if (tamper_detection.ValidateChromeProxyHeader(chrome_proxy_fingerprint)) { 66 tamper_detection.ReportUMAforChromeProxyHeaderValidation(); 67 return true; 68 } 69 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( 74 scheme_is_https, 75 "DataReductionProxy.HeaderTamperDetectionHTTPS", 76 "DataReductionProxy.HeaderTamperDetectionHTTP", 77 carrier_id); 78 79 bool tampered = false; 80 std::string fingerprint; 81 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); 88 tampered = true; 89 } 90 } 91 92 if (GetDataReductionProxyActionFingerprintOtherHeaders( 93 headers, &fingerprint)) { 94 if (tamper_detection.ValidateOtherHeaders(fingerprint)) { 95 tamper_detection.ReportUMAforOtherHeadersValidation(); 96 tampered = true; 97 } 98 } 99 100 if (GetDataReductionProxyActionFingerprintContentLength( 101 headers, &fingerprint)) { 102 if (tamper_detection.ValidateContentLengthHeader(fingerprint)) { 103 tamper_detection.ReportUMAforContentLengthHeaderValidation(); 104 tampered = true; 105 } 106 } 107 108 if (!tampered) { 109 REPORT_TAMPER_DETECTION_UMA( 110 scheme_is_https, 111 "DataReductionProxy.HeaderTamperDetectionPassHTTPS", 112 "DataReductionProxy.HeaderTamperDetectionPassHTTP", 113 carrier_id); 114 } 115 116 return tampered; 117 } 118 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) { 127 DCHECK(headers); 128 } 129 130 DataReductionProxyTamperDetection::~DataReductionProxyTamperDetection() {}; 131 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)) 139 return true; 140 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); 145 146 // Calculates the MD5 hash value of Chrome-Proxy. 147 std::string actual_fingerprint; 148 GetMD5(ValuesToSortedString(&chrome_proxy_header_values), 149 &actual_fingerprint); 150 151 return received_fingerprint != actual_fingerprint; 152 } 153 154 void DataReductionProxyTamperDetection:: 155 ReportUMAforChromeProxyHeaderValidation() const { 156 REPORT_TAMPER_DETECTION_UMA( 157 scheme_is_https_, 158 "DataReductionProxy.HeaderTamperedHTTPS_ChromeProxy", 159 "DataReductionProxy.HeaderTamperedHTTP_ChromeProxy", 160 carrier_id_); 161 } 162 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( 171 response_headers_, 172 &has_intermediary); 173 174 if (*has_chrome_proxy_via_header) 175 return !has_intermediary; 176 return true; 177 } 178 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( 184 scheme_is_https_, 185 "DataReductionProxy.HeaderTamperedHTTPS_Via_Missing", 186 "DataReductionProxy.HeaderTamperedHTTP_Via_Missing", 187 carrier_id_); 188 return; 189 } 190 191 REPORT_TAMPER_DETECTION_UMA( 192 scheme_is_https_, 193 "DataReductionProxy.HeaderTamperedHTTPS_Via", 194 "DataReductionProxy.HeaderTamperedHTTP_Via", 195 carrier_id_); 196 } 197 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:... 207 // 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()); 216 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(), '|'); 222 223 // The first value is the base64 encoded fingerprint. 224 std::string received_fingerprint; 225 if (!it.GetNext() || 226 !base::Base64Decode(it.value(), &received_fingerprint)) { 227 NOTREACHED(); 228 return true; 229 } 230 231 std::string header_values; 232 // The following values are the header names included in the fingerprint 233 // calculation. 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) + ";"; 243 } 244 245 // Calculates the MD5 hash of the concatenated string. 246 std::string actual_fingerprint; 247 GetMD5(header_values, &actual_fingerprint); 248 249 return received_fingerprint != actual_fingerprint; 250 } 251 252 void DataReductionProxyTamperDetection:: 253 ReportUMAforOtherHeadersValidation() const { 254 REPORT_TAMPER_DETECTION_UMA( 255 scheme_is_https_, 256 "DataReductionProxy.HeaderTamperedHTTPS_OtherHeaders", 257 "DataReductionProxy.HeaderTamperedHTTP_OtherHeaders", 258 carrier_id_); 259 } 260 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)) 270 return false; 271 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)) { 276 return false; 277 } 278 279 // Abort, if the Content-Length value cannot be converted to integer. 280 if (!base::StringToInt(actual_content_length_string, 281 &actual_content_length)) { 282 return false; 283 } 284 285 return received_content_length_fingerprint != actual_content_length; 286 } 287 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( 293 scheme_is_https_, 294 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength", 295 "DataReductionProxy.HeaderTamperedHTTP_ContentLength", 296 carrier_id_); 297 298 // Gets MIME type. 299 std::string mime_type; 300 response_headers_->GetMimeType(&mime_type); 301 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/"; 307 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( 316 scheme_is_https_, 317 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength_JS", 318 "DataReductionProxy.HeaderTamperedHTTP_ContentLength_JS", 319 carrier_id_); 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( 324 scheme_is_https_, 325 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength_CSS", 326 "DataReductionProxy.HeaderTamperedHTTP_ContentLength_CSS", 327 carrier_id_); 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( 332 scheme_is_https_, 333 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength_Image", 334 "DataReductionProxy.HeaderTamperedHTTP_ContentLength_Image", 335 carrier_id_); 336 } else { 337 REPORT_TAMPER_DETECTION_UMA( 338 scheme_is_https_, 339 "DataReductionProxy.HeaderTamperedHTTPS_ContentLength_Other", 340 "DataReductionProxy.HeaderTamperedHTTP_ContentLength_Other", 341 carrier_id_); 342 } 343 } 344 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; 353 DCHECK(values); 354 if (!values) return ""; 355 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] + ","; 360 } 361 return concatenated_values; 362 } 363 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)); 370 } 371 372 std::vector<std::string> DataReductionProxyTamperDetection::GetHeaderValues( 373 const net::HttpResponseHeaders* headers, 374 const std::string& header_name) { 375 std::vector<std::string> values; 376 std::string value; 377 void* iter = NULL; 378 while (headers->EnumerateHeader(&iter, header_name, &value)) { 379 values.push_back(value); 380 } 381 return values; 382 } 383 384 } // namespace data_reduction_proxy 385