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 | |
|
Tom Bergan
2017/06/07 22:18:08
Directives should be comma-separated, I think?
Robert Ogden
2017/06/07 23:38:35
Done.
| |
| 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 "foo", | |
| 102 "{{RAND_STR(10)}}", | |
| 103 "page-policies", | |
| 104 "q", | |
| 105 "block-once", | |
| 106 "block", | |
| 107 "bypass", | |
| 108 ], | |
| 109 "directive_values": [ | |
| 110 "bar", | |
| 111 "{{RAND_INT(1)}}", | |
| 112 "{{RAND_INT(4)}}", | |
| 113 "empty-image", | |
| 114 "{{RAND_STR(10)}}", | |
| 115 "low", | |
| 116 "preview", | |
| 117 ], | |
| 118 "max_directives": 10, | |
|
Tom Bergan
2017/06/07 22:18:08
You can probably shrink this to 3, since most bugs
Robert Ogden
2017/06/07 23:38:36
Done.
| |
| 119 "max_directive_values": 10, | |
|
Tom Bergan
2017/06/07 22:18:08
Ditto here.
Robert Ogden
2017/06/07 23:38:35
Done.
| |
| 120 "directive_value_joiner": "|", | |
| 121 }, | |
| 122 } | |
| 123 | |
| 124 TEST_SERVER = "chromeproxy-test.appspot.com" | |
| 125 | |
| 126 # These headers will be present in every test server response. If one of these | |
| 127 # entries is also a fuzzed header above, then the fuzzed value will take the | |
| 128 # place of the static one instead. | |
| 129 STATIC_RESPONSE_HEADERS = { | |
| 130 "content-type": ["text/html"], | |
| 131 "via": ["1.1 Chrome-Compression-Proxy"], | |
| 132 "cache-control": ["no-cache, no-store, must-revalidate"], | |
| 133 "pragma": ["no-cache"], | |
| 134 "expires": ["0"], | |
| 135 } | |
| 136 | |
| 137 # This string will be used as the response body in every test and will be | |
| 138 # checked for existance on the final loaded page. | |
| 139 STATIC_RESPONSE_BODY = 'ok' | |
| 140 | |
| 141 rand_str_re = re.compile(r'{{RAND_STR\((\d+)\)}}') | |
| 142 rand_int_re = re.compile(r'{{RAND_INT\((\d+)\)}}') | |
| 143 rand_dbl_re = re.compile(r'{{RAND_DBL\((\d+)\.(\d+)\)}}') | |
| 144 rand_bool_re = re.compile(r'{{RAND_BOOL}}') | |
| 145 | |
| 146 def ParseRand(key, val): | |
| 147 """This helper function parses the {{RAND}} expressions in the given values | |
| 148 and returns them with random values subsituted in place. | |
| 149 | |
| 150 Args: | |
| 151 key: the header key with 0 or more {{RAND}} expressions | |
| 152 val: the header value with 0 or more {{RAND}} expressions | |
| 153 Returns: | |
| 154 A key, value tuple with subsituted random values | |
| 155 """ | |
| 156 def GenerateRand(length, charset): | |
| 157 return ''.join(random.choice(charset) for _ in range(length)) | |
| 158 def _parse_rand(v): | |
| 159 result = v | |
| 160 had_match = True | |
| 161 while had_match: | |
| 162 had_match = False | |
| 163 str_match = rand_str_re.search(result) | |
| 164 if str_match: | |
| 165 had_match = True | |
| 166 mag = int(result[str_match.start(1):str_match.end(1)]) | |
| 167 rand_str = GenerateRand(mag, string.ascii_lowercase + string.digits) | |
| 168 result = (result[:str_match.start()] + rand_str | |
| 169 + result[str_match.end():]) | |
| 170 int_match = rand_int_re.search(result) | |
| 171 if int_match: | |
| 172 had_match = True | |
| 173 mag = int(result[int_match.start(1):int_match.end(1)]) | |
| 174 rand_int = GenerateRand(mag, string.digits) | |
| 175 result = (result[:int_match.start()] + rand_int | |
| 176 + result[int_match.end():]) | |
| 177 dbl_match = rand_dbl_re.search(result) | |
| 178 if dbl_match: | |
| 179 had_match = True | |
| 180 magN = int(result[dbl_match.start(1):dbl_match.end(1)]) | |
| 181 magD = int(result[dbl_match.start(2):dbl_match.end(2)]) | |
| 182 rand_dbl = GenerateRand(magN, string.digits) + '.' + GenerateRand(magD, | |
| 183 string.digits) | |
| 184 result = (result[:dbl_match.start()] + rand_dbl | |
| 185 + result[dbl_match.end():]) | |
| 186 bool_match = rand_bool_re.search(result) | |
| 187 if bool_match: | |
| 188 had_match = True | |
| 189 rand_bool = bool(random.getrandbits(1)) | |
| 190 result = (result[:bool_match.start()] + str(rand_bool).lower() | |
| 191 + result[bool_match.end():]) | |
| 192 return result | |
| 193 return (_parse_rand(key), _parse_rand(val)) | |
| 194 | |
| 195 def GenerateFuzzedHeaders(cfg=FUZZ_HEADERS): | |
| 196 """This function yields header key value pairs which can be used to update a | |
| 197 Python dict representing HTTP headers. See file level documentation for more | |
| 198 information. | |
| 199 | |
| 200 Args: | |
| 201 cfg: the configuration dict that specifies how to fuzz the proxy headers | |
| 202 Yields: | |
| 203 one header key value pair | |
| 204 """ | |
| 205 for header_key in cfg: | |
| 206 fuzz = cfg[header_key] | |
| 207 dirs = fuzz['directive_keys'] | |
| 208 vals = fuzz['directive_values'] | |
| 209 max_dirs = min(fuzz['max_directives'], len(dirs)) | |
| 210 max_vals = min(fuzz['max_directive_values'], len(vals)) | |
| 211 def GenerateFuzzedValues(): | |
| 212 for n in range(0, max_vals + 1): | |
| 213 for c in itertools.combinations(vals, n): | |
| 214 yield c | |
| 215 # Yield an empty header key,value pair before doing all the combinations. | |
| 216 yield (header_key, '') | |
| 217 for num_dirs in range(1, max_dirs + 1): | |
| 218 for directive_set in itertools.combinations(dirs, num_dirs): | |
| 219 for values in GenerateFuzzedValues(): | |
| 220 value_list = list(values) | |
| 221 if '' in value_list: | |
| 222 value_list.remove('') | |
| 223 value_str = fuzz['directive_value_joiner'].join(value_list) | |
| 224 header = [] | |
| 225 for directive in directive_set: | |
| 226 if len(value_str) == 0: | |
| 227 header.append(directive) | |
| 228 else: | |
| 229 header.append('%s=%s' % (directive, value_str)) | |
| 230 yield ParseRand(header_key, ' '.join(header)) | |
| 231 | |
| 232 class FuzzUnitTests(IntegrationTest): | |
| 233 | |
| 234 def testParseRand(self): | |
| 235 tests = { | |
| 236 "{{RAND_STR(1)}": r"{{RAND_STR\(1\)}", | |
| 237 "{{RAND_INT(1)}": r"{{RAND_INT\(1\)}", | |
| 238 "{{RAND_DBL(1.1)}": r"{{RAND_DBL\(1\.1\)}", | |
| 239 "{{RAND_DBL(11)}}": r"{{RAND_DBL\(11\)}}", | |
| 240 "{{RAND_BOOL}": r"{{RAND_BOOL}", | |
| 241 "{{RAND_STR(0)}}": "", | |
| 242 "hi{{RAND_STR(0)}}": "hi", | |
| 243 "{{RAND_STR(0)}}there": "there", | |
| 244 "hi{{RAND_STR(0)}}there": "hithere", | |
| 245 "{{RAND_STR(3)}}": r"[a-z0-9][a-z0-9][a-z0-9]", | |
| 246 "{{RAND_STR(3)}}there": r"[a-z0-9][a-z0-9][a-z0-9]there", | |
| 247 "hi{{RAND_STR(3)}}": r"hi[a-z0-9][a-z0-9][a-z0-9]", | |
| 248 "hi{{RAND_STR(3)}}there": r"hi[a-z0-9][a-z0-9][a-z0-9]there", | |
| 249 "{{RAND_INT(0)}}": "", | |
| 250 "hi{{RAND_INT(0)}}": "hi", | |
| 251 "{{RAND_INT(0)}}there": "there", | |
| 252 "hi{{RAND_INT(0)}}there": "hithere", | |
| 253 "{{RAND_INT(3)}}": r"[0-9][0-9][0-9]", | |
| 254 "{{RAND_INT(3)}}there": r"[0-9][0-9][0-9]there", | |
| 255 "hi{{RAND_INT(3)}}": r"hi[0-9][0-9][0-9]", | |
| 256 "hi{{RAND_INT(3)}}there": r"hi[0-9][0-9][0-9]there", | |
| 257 "{{RAND_DBL(0.0)}}": r"\.", | |
| 258 "hi{{RAND_DBL(0.0)}}": r"hi\.", | |
| 259 "{{RAND_DBL(0.0)}}there": r"\.there", | |
| 260 "hi{{RAND_DBL(0.0)}}there": r"hi\.there", | |
| 261 "{{RAND_DBL(3.3)}}": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]", | |
| 262 "hi{{RAND_DBL(3.3)}}": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]", | |
| 263 "{{RAND_DBL(3.3)}}there": r"[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", | |
| 264 "hi{{RAND_DBL(3.3)}}there": r"hi[0-9][0-9][0-9]\.[0-9][0-9][0-9]there", | |
| 265 "{{RAND_BOOL}}": r"(true|false)", | |
| 266 "{{RAND_BOOL}}there": r"(true|false)there", | |
| 267 "hi{{RAND_BOOL}}": r"hi(true|false)", | |
| 268 "hi{{RAND_BOOL}}there": r"hi(true|false)there", | |
| 269 "{{RAND_STR(1)}}{{RAND_STR(1)}}": r"[a-z0-9][a-z0-9]", | |
| 270 "{{RAND_STR(1)}}{{RAND_INT(1)}}": r"[a-z0-9][0-9]", | |
| 271 "{{RAND_STR(1)}}{{RAND_DBL(1.1)}}": r"[a-z0-9][0-9]\.[0-9]", | |
| 272 "{{RAND_STR(1)}}{{RAND_BOOL}}": r"[a-z0-9](true|false)", | |
| 273 "{{RAND_INT(1)}}{{RAND_STR(1)}}": r"[0-9][a-z0-9]", | |
| 274 "{{RAND_INT(1)}}{{RAND_INT(1)}}": r"[0-9][0-9]", | |
| 275 "{{RAND_INT(1)}}{{RAND_DBL(1.1)}}": r"[0-9][0-9]\.[0-9]", | |
| 276 "{{RAND_INT(1)}}{{RAND_BOOL}}": r"[0-9](true|false)", | |
| 277 "{{RAND_DBL(1.1)}}{{RAND_STR(1)}}": r"[0-9]\.[0-9][a-z0-9]", | |
| 278 "{{RAND_DBL(1.1)}}{{RAND_INT(1)}}": r"[0-9]\.[0-9][0-9]", | |
| 279 "{{RAND_DBL(1.1)}}{{RAND_DBL(1.1)}}": r"[0-9]\.[0-9][0-9]\.[0-9]", | |
| 280 "{{RAND_DBL(1.1)}}{{RAND_BOOL}}": r"[0-9]\.[0-9](true|false)", | |
| 281 "{{RAND_BOOL}}{{RAND_STR(1)}}": r"(true|false)[a-z0-9]", | |
| 282 "{{RAND_BOOL}}{{RAND_INT(1)}}": r"(true|false)[0-9]", | |
| 283 "{{RAND_BOOL}}{{RAND_DBL(1.1)}}": r"(true|false)[0-9]\.[0-9]", | |
| 284 "{{RAND_BOOL}}{{RAND_BOOL}}": r"(true|false)(true|false)", | |
| 285 } | |
| 286 for t in tests: | |
| 287 expected = re.compile('^' + tests[t] + '$') | |
| 288 gotK, gotV = ParseRand(t, t) | |
| 289 if not expected.match(gotK): | |
| 290 self.fail("%s doesn't match /%s/" % (gotK, tests[t])) | |
| 291 if not expected.match(gotV): | |
| 292 self.fail("%s doesn't match /%s/" % (gotK, tests[t])) | |
| 293 | |
| 294 def testGenerator(self): | |
| 295 test_cfg = { | |
| 296 "chrome-proxy": { | |
| 297 "directive_keys": [ | |
| 298 "foo", | |
| 299 "page_policies", | |
| 300 ], | |
| 301 "directive_values": [ | |
| 302 "bar", | |
| 303 "empty-image", | |
| 304 ], | |
| 305 "max_directives": 10, | |
| 306 "max_directive_values": 10, | |
| 307 "directive_value_joiner": "|", | |
| 308 }, | |
| 309 } | |
| 310 expected_headers = ['', 'foo', 'foo=bar', 'foo=empty-image', | |
| 311 'foo=bar|empty-image', 'page_policies', 'page_policies=bar', | |
| 312 'page_policies=empty-image', 'page_policies=bar|empty-image', | |
| 313 'foo page_policies', 'foo=bar page_policies=bar', | |
| 314 'foo=empty-image page_policies=empty-image', | |
| 315 'foo=bar|empty-image page_policies=bar|empty-image', | |
| 316 ] | |
| 317 actual_headers = [] | |
| 318 for h in GenerateFuzzedHeaders(cfg=test_cfg): | |
| 319 actual_headers.append(h[1]) | |
| 320 expected_headers.sort() | |
| 321 actual_headers.sort() | |
| 322 self.assertEqual(expected_headers, actual_headers) | |
| 323 | |
| 324 class ProtocolFuzzer(IntegrationTest): | |
| 325 | |
| 326 def GenerateTestURLs(self): | |
| 327 """This function yields test URLs which will cause the test server to | |
| 328 respond with the given given headers and body. | |
| 329 | |
| 330 Yields: | |
| 331 URLs suitable for testing fuzzed response headers | |
| 332 """ | |
| 333 for fz_key, fz_val in GenerateFuzzedHeaders(): | |
| 334 headers = {} | |
| 335 headers.update(STATIC_RESPONSE_HEADERS) | |
| 336 headers.update({fz_key: [fz_val]}) | |
| 337 json_headers = json.dumps(headers) | |
| 338 b64_headers = base64.b64encode(json_headers) | |
| 339 url = "http://%s/default?respBody=%s&respHeader=%s" % (TEST_SERVER, | |
| 340 base64.b64encode(STATIC_RESPONSE_BODY), b64_headers) | |
| 341 yield (json_headers, url) | |
| 342 | |
| 343 @Slow | |
| 344 def testFuzzing(self): | |
| 345 for headers, url in self.GenerateTestURLs(): | |
| 346 with TestDriver() as t: | |
| 347 t.AddChromeArg('--enable-spdy-proxy-auth') | |
| 348 t.AddChromeArg('--data-reduction-proxy-http-proxies=' | |
| 349 'https://chromeproxy-test.appspot.com') | |
| 350 try: | |
| 351 t.LoadURL(url) | |
| 352 # The main test is to make sure Chrome doesn't crash after loading a | |
| 353 # page with fuzzed headers, which would be raised as a ChromeDriver | |
| 354 # exception. Otherwise, we'll do a simple check and make sure the page | |
| 355 # body is correct and Chrome isn't displaying some kind of error page. | |
| 356 body = t.ExecuteJavascriptStatement('document.body.innerHTML') | |
| 357 self.assertEqual(body, STATIC_RESPONSE_BODY) | |
| 358 except Exception as e: | |
| 359 print 'Response headers: ' + headers | |
| 360 print 'URL: ' + url | |
| 361 raise e | |
| 362 | |
| 363 if __name__ == '__main__': | |
| 364 IntegrationTest.RunAllTests() | |
| OLD | NEW |