Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright 2017 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 # | |
| 6 # This test checks that Chrome does not crash or otherwise wildly misbehave when | |
| 7 # it receives unexpected Chrome-Proxy header values and/or directives. This is | |
| 8 # done by fuzz testing which is configured through a dictionary object at the | |
| 9 # top of this file. | |
| 10 # | |
| 11 # The fuzz testing generates URLs which are requested from the Chrome Proxy test | |
| 12 # server which will modify its response based on the encoded data in the URL. | |
| 13 # | |
| 14 # The fuzz testing is configured in a dictionary-type object of the following | |
| 15 # format: | |
| 16 # { | |
| 17 # # This specifies a header that will be fuzzed. Only one header is fuzzed | |
| 18 # # per test. When a header is being fuzz tested, any entry it has in the | |
| 19 # # STATIC_RESPONSE_HEADERS object will be overwritten. | |
| 20 # "chrome-proxy": { | |
| 21 # # This specifies the directive key values that are possible to use in | |
| 22 # # fuzzing. | |
| 23 # "directive_keys: [ | |
| 24 # "page-policy", | |
| 25 # "foo", | |
| 26 # ], | |
| 27 # # This specifies the directive values that are possible to use in | |
| 28 # # fuzzing. Any one of these values may end up mapped to any one of the | |
| 29 # # directive_keys above. | |
| 30 # "directive_values": [ | |
| 31 # "empty-image", | |
| 32 # "bar", | |
| 33 # ], | |
| 34 # # The maximum number of directives to use in this header. If this value | |
| 35 # # is greater than the number of directives given above, that will be | |
| 36 # # used instead. | |
| 37 # "max_directives": 10, | |
| 38 # | |
| 39 # # The maximum number of directive values to use per directive. If this | |
| 40 # # value is greater than the number of directive values given above, that | |
| 41 # # will be used instead. | |
| 42 # "max_directive_values": 10, | |
| 43 # | |
| 44 # # This is used to join the directive values when multiple are used. | |
| 45 # "directive_value_joiner": "|", | |
| 46 # }, | |
| 47 # } | |
| 48 # | |
| 49 # If the above configuration was given, the following values would be generated | |
| 50 # for the chrome-proxy header: | |
| 51 # <empty-string> | |
| 52 # foo | |
| 53 # foo=bar | |
| 54 # foo=empty-image | |
| 55 # foo=bar|empty-image | |
| 56 # page_policies | |
| 57 # page_policies=bar | |
| 58 # page_policies=empty-image | |
| 59 # page_policies=bar|empty-image | |
| 60 # foo,page_policies | |
| 61 # foo=bar,page_policies=bar | |
| 62 # foo=empty-image,page_policies=empty-image | |
| 63 # foo=bar|empty-image,page_policies=bar|empty-image | |
| 64 # | |
| 65 # Randomly generated values are also supported in the fuzz_header field | |
| 66 # ("chrome-proxy" in the above example), directive_keys, and directive_values. | |
| 67 # Randomly generated values can be specified using these formats: | |
| 68 # {{RAND_STR(N)}} | |
| 69 # Creates a random string of lowercase and digit characters of length N | |
| 70 # | |
| 71 # {{RAND_INT(N)}} | |
| 72 # Creates a random integer in the range [0, 10^N+1) with leading zeros | |
| 73 # Example: "Num cookies: {{RAND_INT(4)}}" yields "Num cookies: 0123" | |
| 74 # | |
| 75 # {{RAND_DBL(P.Q)}} | |
| 76 # Creates a random double in the range [0, 10^P+1) with leading zeros and | |
| 77 # up to Q places after the decimal point | |
| 78 # Example: "My money = ${{RAND_DBL(3,2)}}" yields "My money = $001.53" | |
| 79 # | |
| 80 # {{RAND_BOOL}} | |
| 81 # Creates a random boolean, either 'true' or 'false' | |
| 82 # Example: "I am awesome: {{RAND_BOOL}}" yields "I am awesome: true" | |
| 83 | |
| 84 import BaseHTTPServer | |
| 85 import base64 | |
| 86 import itertools | |
| 87 import json | |
| 88 import random | |
| 89 import re | |
| 90 import string | |
| 91 | |
| 92 from common import TestDriver | |
| 93 from common import IntegrationTest | |
| 94 from decorators import Slow | |
| 95 | |
| 96 # This dict configures how the fuzzing will operate. See documentation above for | |
| 97 # more information. | |
| 98 FUZZ_HEADERS = { | |
| 99 "chrome-proxy": { | |
| 100 "directive_keys": [ | |
| 101 "{{RAND_STR(10)}}", | |
| 102 "{{RAND_STR(10)}}", | |
|
Tom Bergan
2017/06/08 15:00:18
Do we actually need two of these?
Robert Ogden
2017/06/08 17:17:58
I was thinking yes so that we get three directives
| |
| 103 "page-policies", | |
| 104 ], | |
| 105 "directive_values": [ | |
| 106 "empty-image", | |
| 107 "{{RAND_INT(1)}}", | |
| 108 "{{RAND_INT(4)}}", | |
| 109 "{{RAND_STR(10)}}", | |
| 110 "{{RAND_STR(10)}}", | |
| 111 "{{RAND_STR(10)}}", | |
|
Tom Bergan
2017/06/08 15:00:18
Ditto here
Robert Ogden
2017/06/08 17:17:58
Yes, so that there are three random strings in at
| |
| 112 ], | |
| 113 "max_directives": 3, | |
| 114 "max_directive_values": 3, | |
| 115 "directive_value_joiner": "|", | |
| 116 }, | |
| 117 } | |
| 118 | |
| 119 TEST_SERVER = "chromeproxy-test.appspot.com" | |
| 120 | |
| 121 # These headers will be present in every test server response. If one of these | |
| 122 # entries is also a fuzzed header above, then the fuzzed value will take the | |
| 123 # place of the static one instead. | |
| 124 STATIC_RESPONSE_HEADERS = { | |
| 125 "content-type": ["text/html"], | |
| 126 "via": ["1.1 Chrome-Compression-Proxy"], | |
| 127 "cache-control": ["no-cache, no-store, must-revalidate"], | |
| 128 "pragma": ["no-cache"], | |
| 129 "expires": ["0"], | |
| 130 } | |
| 131 | |
| 132 # This string will be used as the response body in every test and will be | |
| 133 # checked for existance on the final loaded page. | |
| 134 STATIC_RESPONSE_BODY = 'ok' | |
| 135 | |
| 136 rand_str_re = re.compile(r'{{RAND_STR\((\d+)\)}}') | |
| 137 rand_int_re = re.compile(r'{{RAND_INT\((\d+)\)}}') | |
| 138 rand_dbl_re = re.compile(r'{{RAND_DBL\((\d+)\.(\d+)\)}}') | |
| 139 rand_bool_re = re.compile(r'{{RAND_BOOL}}') | |
| 140 | |
| 141 def ParseRand(key, val): | |
| 142 """This helper function parses the {{RAND}} expressions in the given values | |
| 143 and returns them with random values subsituted in place. | |
| 144 | |
| 145 Args: | |
| 146 key: the header key with 0 or more {{RAND}} expressions | |
| 147 val: the header value with 0 or more {{RAND}} expressions | |
| 148 Returns: | |
| 149 A key, value tuple with subsituted random values | |
| 150 """ | |
| 151 def GenerateRand(length, charset): | |
| 152 return ''.join(random.choice(charset) for _ in range(length)) | |
| 153 def _parse_rand(v): | |
| 154 result = v | |
| 155 had_match = True | |
| 156 while had_match: | |
| 157 had_match = False | |
| 158 str_match = rand_str_re.search(result) | |
| 159 if str_match: | |
| 160 had_match = True | |
| 161 mag = int(result[str_match.start(1):str_match.end(1)]) | |
| 162 rand_str = GenerateRand(mag, string.ascii_lowercase + string.digits) | |
| 163 result = (result[:str_match.start()] + rand_str | |
| 164 + result[str_match.end():]) | |
| 165 int_match = rand_int_re.search(result) | |
| 166 if int_match: | |
| 167 had_match = True | |
| 168 mag = int(result[int_match.start(1):int_match.end(1)]) | |
| 169 rand_int = GenerateRand(mag, string.digits) | |
| 170 result = (result[:int_match.start()] + rand_int | |
| 171 + result[int_match.end():]) | |
| 172 dbl_match = rand_dbl_re.search(result) | |
| 173 if dbl_match: | |
| 174 had_match = True | |
| 175 magN = int(result[dbl_match.start(1):dbl_match.end(1)]) | |
| 176 magD = int(result[dbl_match.start(2):dbl_match.end(2)]) | |
| 177 rand_dbl = GenerateRand(magN, string.digits) + '.' + GenerateRand(magD, | |
| 178 string.digits) | |
| 179 result = (result[:dbl_match.start()] + rand_dbl | |
| 180 + result[dbl_match.end():]) | |
| 181 bool_match = rand_bool_re.search(result) | |
| 182 if bool_match: | |
| 183 had_match = True | |
| 184 rand_bool = bool(random.getrandbits(1)) | |
| 185 result = (result[:bool_match.start()] + str(rand_bool).lower() | |
| 186 + result[bool_match.end():]) | |
| 187 return result | |
| 188 return (_parse_rand(key), _parse_rand(val)) | |
| 189 | |
| 190 def GenerateFuzzedHeaders(cfg=FUZZ_HEADERS): | |
| 191 """This function yields header key value pairs which can be used to update a | |
| 192 Python dict representing HTTP headers. See file level documentation for more | |
| 193 information. | |
| 194 | |
| 195 Args: | |
| 196 cfg: the configuration dict that specifies how to fuzz the proxy headers | |
| 197 Yields: | |
| 198 one header key value pair | |
| 199 """ | |
| 200 for header_key in cfg: | |
| 201 fuzz = cfg[header_key] | |
| 202 dirs = fuzz['directive_keys'] | |
| 203 vals = fuzz['directive_values'] | |
| 204 max_dirs = min(fuzz['max_directives'], len(dirs)) | |
| 205 max_vals = min(fuzz['max_directive_values'], len(vals)) | |
| 206 def GenerateFuzzedValues(): | |
| 207 for n in range(0, max_vals + 1): | |
| 208 for c in itertools.combinations(vals, n): | |
| 209 yield c | |
| 210 # Yield an empty header key,value pair before doing all the combinations. | |
| 211 yield (header_key, '') | |
| 212 for num_dirs in range(1, max_dirs + 1): | |
| 213 for directive_set in itertools.combinations(dirs, num_dirs): | |
| 214 for values in GenerateFuzzedValues(): | |
| 215 value_list = list(values) | |
| 216 if '' in value_list: | |
| 217 value_list.remove('') | |
| 218 value_str = fuzz['directive_value_joiner'].join(value_list) | |
| 219 header = [] | |
| 220 for directive in directive_set: | |
| 221 if len(value_str) == 0: | |
| 222 header.append(directive) | |
| 223 else: | |
| 224 header.append('%s=%s' % (directive, value_str)) | |
| 225 yield ParseRand(header_key, ','.join(header)) | |
| 226 | |
| 227 class FuzzUnitTests(IntegrationTest): | |
| 228 | |
| 229 def testParseRand(self): | |
| 230 tests = { | |
| 231 "{{RAND_STR(1)}": r"{{RAND_STR\(1\)}", | |
| 232 "{{RAND_INT(1)}": r"{{RAND_INT\(1\)}", | |
| 233 "{{RAND_DBL(1.1)}": r"{{RAND_DBL\(1\.1\)}", | |
| 234 "{{RAND_DBL(11)}}": r"{{RAND_DBL\(11\)}}", | |
| 235 "{{RAND_BOOL}": r"{{RAND_BOOL}", | |
| 236 "{{RAND_STR(0)}}": "", | |
| 237 "hi{{RAND_STR(0)}}": "hi", | |
| 238 "{{RAND_STR(0)}}there": "there", | |
| 239 "hi{{RAND_STR(0)}}there": "hithere", | |
| 240 "{{RAND_STR(3)}}": r"[a-z0-9][a-z0-9][a-z0-9]", | |
| 241 "{{RAND_STR(3)}}there": r"[a-z0-9][a-z0-9][a-z0-9]there", | |
| 242 "hi{{RAND_STR(3)}}": r"hi[a-z0-9][a-z0-9][a-z0-9]", | |
| 243 "hi{{RAND_STR(3)}}there": r"hi[a-z0-9][a-z0-9][a-z0-9]there", | |
| 244 "{{RAND_INT(0)}}": "", | |
| 245 "hi{{RAND_INT(0)}}": "hi", | |
| 246 "{{RAND_INT(0)}}there": "there", | |
| 247 "hi{{RAND_INT(0)}}there": "hithere", | |
| 248 "{{RAND_INT(3)}}": r"[0-9][0-9][0-9]", | |
| 249 "{{RAND_INT(3)}}there": r"[0-9][0-9][0-9]there", | |
| 250 "hi{{RAND_INT(3)}}": r"hi[0-9][0-9][0-9]", | |
| 251 "hi{{RAND_INT(3)}}there": r"hi[0-9][0-9][0-9]there", | |
| 252 "{{RAND_DBL(0.0)}}": r"\.", | |
| 253 "hi{{RAND_DBL(0.0)}}": r"hi\.", | |
| 254 "{{RAND_DBL(0.0)}}there": r"\.there", | |
| 255 "hi{{RAND_DBL(0.0)}}there": r"hi\.there", | |
| 256 "{{RAND_DBL(3.3)}}": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]", | |
| 257 "hi{{RAND_DBL(3.3)}}": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]", | |
| 258 "{{RAND_DBL(3.3)}}there": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", | |
| 259 "hi{{RAND_DBL(3.3)}}there": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", | |
| 260 "{{RAND_BOOL}}": r"(true|false)", | |
| 261 "{{RAND_BOOL}}there": r"(true|false)there", | |
| 262 "hi{{RAND_BOOL}}": r"hi(true|false)", | |
| 263 "hi{{RAND_BOOL}}there": r"hi(true|false)there", | |
| 264 "{{RAND_STR(1)}}{{RAND_STR(1)}}": r"[a-z0-9][a-z0-9]", | |
| 265 "{{RAND_STR(1)}}{{RAND_INT(1)}}": r"[a-z0-9][0-9]", | |
| 266 "{{RAND_STR(1)}}{{RAND_DBL(1.1)}}": r"[a-z0-9][0-9]\.[0-9]", | |
| 267 "{{RAND_STR(1)}}{{RAND_BOOL}}": r"[a-z0-9](true|false)", | |
| 268 "{{RAND_INT(1)}}{{RAND_STR(1)}}": r"[0-9][a-z0-9]", | |
| 269 "{{RAND_INT(1)}}{{RAND_INT(1)}}": r"[0-9][0-9]", | |
| 270 "{{RAND_INT(1)}}{{RAND_DBL(1.1)}}": r"[0-9][0-9]\.[0-9]", | |
| 271 "{{RAND_INT(1)}}{{RAND_BOOL}}": r"[0-9](true|false)", | |
| 272 "{{RAND_DBL(1.1)}}{{RAND_STR(1)}}": r"[0-9]\.[0-9][a-z0-9]", | |
| 273 "{{RAND_DBL(1.1)}}{{RAND_INT(1)}}": r"[0-9]\.[0-9][0-9]", | |
| 274 "{{RAND_DBL(1.1)}}{{RAND_DBL(1.1)}}": r"[0-9]\.[0-9][0-9]\.[0-9]", | |
| 275 "{{RAND_DBL(1.1)}}{{RAND_BOOL}}": r"[0-9]\.[0-9](true|false)", | |
| 276 "{{RAND_BOOL}}{{RAND_STR(1)}}": r"(true|false)[a-z0-9]", | |
| 277 "{{RAND_BOOL}}{{RAND_INT(1)}}": r"(true|false)[0-9]", | |
| 278 "{{RAND_BOOL}}{{RAND_DBL(1.1)}}": r"(true|false)[0-9]\.[0-9]", | |
| 279 "{{RAND_BOOL}}{{RAND_BOOL}}": r"(true|false)(true|false)", | |
| 280 } | |
| 281 for t in tests: | |
| 282 expected = re.compile('^' + tests[t] + '$') | |
| 283 gotK, gotV = ParseRand(t, t) | |
| 284 if not expected.match(gotK): | |
| 285 self.fail("%s doesn't match /%s/" % (gotK, tests[t])) | |
| 286 if not expected.match(gotV): | |
| 287 self.fail("%s doesn't match /%s/" % (gotK, tests[t])) | |
| 288 | |
| 289 def testGenerator(self): | |
| 290 test_cfg = { | |
| 291 "chrome-proxy": { | |
| 292 "directive_keys": [ | |
| 293 "foo", | |
| 294 "page_policies", | |
| 295 ], | |
| 296 "directive_values": [ | |
| 297 "bar", | |
| 298 "empty-image", | |
| 299 ], | |
| 300 "max_directives": 10, | |
| 301 "max_directive_values": 10, | |
| 302 "directive_value_joiner": "|", | |
| 303 }, | |
| 304 } | |
| 305 expected_headers = ['', 'foo', 'foo=bar', 'foo=empty-image', | |
| 306 'foo=bar|empty-image', 'page_policies', 'page_policies=bar', | |
| 307 'page_policies=empty-image', 'page_policies=bar|empty-image', | |
| 308 'foo,page_policies', 'foo=bar,page_policies=bar', | |
| 309 'foo=empty-image,page_policies=empty-image', | |
| 310 'foo=bar|empty-image,page_policies=bar|empty-image', | |
| 311 ] | |
| 312 actual_headers = [] | |
| 313 for h in GenerateFuzzedHeaders(cfg=test_cfg): | |
| 314 actual_headers.append(h[1]) | |
| 315 expected_headers.sort() | |
| 316 actual_headers.sort() | |
| 317 self.assertEqual(expected_headers, actual_headers) | |
| 318 | |
| 319 class ProtocolFuzzer(IntegrationTest): | |
| 320 | |
| 321 def GenerateTestURLs(self): | |
| 322 """This function yields test URLs which will cause the test server to | |
| 323 respond with the given given headers and body. | |
| 324 | |
| 325 Yields: | |
| 326 URLs suitable for testing fuzzed response headers | |
| 327 """ | |
| 328 for fz_key, fz_val in GenerateFuzzedHeaders(): | |
| 329 headers = {} | |
| 330 headers.update(STATIC_RESPONSE_HEADERS) | |
| 331 headers.update({fz_key: [fz_val]}) | |
| 332 json_headers = json.dumps(headers) | |
| 333 b64_headers = base64.b64encode(json_headers) | |
| 334 url = "http://%s/default?respBody=%s&respHeader=%s" % (TEST_SERVER, | |
| 335 base64.b64encode(STATIC_RESPONSE_BODY), b64_headers) | |
| 336 yield (json_headers, url) | |
| 337 | |
| 338 @Slow | |
| 339 def testFuzzing(self): | |
| 340 with TestDriver() as t: | |
| 341 t.AddChromeArg('--enable-spdy-proxy-auth') | |
| 342 t.AddChromeArg('--data-reduction-proxy-http-proxies=' | |
| 343 'https://chromeproxy-test.appspot.com') | |
| 344 for headers, url in self.GenerateTestURLs(): | |
| 345 try: | |
| 346 t.LoadURL(url) | |
| 347 # The main test is to make sure Chrome doesn't crash after loading a | |
| 348 # page with fuzzed headers, which would be raised as a ChromeDriver | |
| 349 # exception. Otherwise, we'll do a simple check and make sure the page | |
| 350 # body is correct and Chrome isn't displaying some kind of error page. | |
| 351 body = t.ExecuteJavascriptStatement('document.body.innerHTML') | |
| 352 self.assertEqual(body, STATIC_RESPONSE_BODY) | |
| 353 except Exception as e: | |
| 354 print 'Response headers: ' + headers | |
| 355 print 'URL: ' + url | |
| 356 raise e | |
| 357 | |
| 358 if __name__ == '__main__': | |
| 359 IntegrationTest.RunAllTests() | |
| OLD | NEW |